Sync current dev state
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

This commit is contained in:
SamiAhmed7777
2025-12-15 17:28:21 -08:00
parent 3bf64b1058
commit f0f346deb9
54 changed files with 11060 additions and 484 deletions

View File

@@ -47,6 +47,8 @@ require_once 'f_core/f_functions/functions.queue.php';
require_once 'f_core/f_functions/functions.videoprocessing.php';
require_once 'f_core/f_functions/functions.rbac.php';
require_once 'f_core/f_functions/functions.branding.php';
require_once 'f_core/f_functions/functions.session.php';
require_once 'f_core/f_functions/functions.api.php';
//class init
$class_filter = new VFilter;
$class_language = new VLanguage;

View File

@@ -18,7 +18,7 @@ defined('_ISVALID') or header("Location: /error");
---- edit
*/
$COOKIE_VALIDATION = false; //BETA feature in testing phase, keep disabled for now
$COOKIE_DOMAIN = '.easystreamdemo.com';
$COOKIE_DOMAIN = ''; // Empty string allows cookies to work with any domain (localhost, IP, domain name)
$COOKIE_WHITELIST = array('127.0.0.1');
/*
---- end edit
@@ -36,37 +36,58 @@ set_include_path($main_dir);
if (!defined('_INCLUDE')) {
define('_INCLUDE', true);
}
define('REM_ADDR', (isset($_SERVER["HTTP_X_FORWARDED_FOR"]) ? 'HTTP_X_FORWARDED_FOR' : 'REMOTE_ADDR'));
define('ENC_FIRSTKEY', '4xR5Zlcwo8uUxyrdA5ykgFUXXQFV32o7abJiv+SBzBqXLCAmPq+ciq2ik1M32aGx8f/PZuNxHZ3uckPF/8BL2w==');
define('ENC_SECONDKEY', 'sH7ZuZ0jsiq9DKvjHHzQWAJaB1Ypav17v1rXVxyXpJSCI0untO8B1BUaUT7jxN2YlnyLy2e/JPJO3hMPSneJhhfQbV+ifrWIgD9JmubK+8PDTzB4gM9C0lV1g5R00KQmHWJ0iScv/oXldB0y6nMnLjiVhnTGNwf6gq1JEvukfac=');
// define('CA_CERT', '/etc/ssl/certs/cacert.pm');
define('COOKIE_VALIDATION', $COOKIE_VALIDATION);
define('COOKIE_DOMAIN', $COOKIE_DOMAIN);
define('COOKIE_VALIDATION_WHITELIST', $COOKIE_WHITELIST);
define('COOKIE_LOG', $main_dir . '/f_data/data_logs/log_error/log_cookie/' . date("Ymd") . "-cookie.log");
define('REQUEST_LOG', $main_dir . '/f_data/data_logs/log_error/log_request/' . date("Ymd") . "-request.log");
define('LIVE_AUTH_LOG', $main_dir . '/f_data/data_logs/log_error/log_live/' . date("Ymd") . "-auth.log");
define('LIVE_DONE_LOG', $main_dir . '/f_data/data_logs/log_error/log_live/' . date("Ymd") . "-done.log");
define('LIVE_REC_LOG', $main_dir . '/f_data/data_logs/log_error/log_live/' . date("Ymd") . "-rec.log");
// Detect HTTPS (supports reverse proxies) to decide secure cookies dynamically
$IS_HTTPS = (
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') ||
(isset($_SERVER['REQUEST_SCHEME']) && strtolower($_SERVER['REQUEST_SCHEME']) === 'https')
);
// Environment-based SameSite policy: Strict on HTTPS in production, Lax in development
$APP_ENV = getenv('APP_ENV') ?: ((isset($_SERVER['HTTP_HOST']) && preg_match('/(localhost|127\.0\.0\.1)$/', $_SERVER['HTTP_HOST'])) ? 'development' : 'production');
$SAMESITE_POLICY = ($APP_ENV === 'production' && $IS_HTTPS) ? 'Strict' : 'Lax';
define('SET_COOKIE_OPTIONS', array(
'expires' => time() + 60 * 60 * 24 * 10, //10 days
'path' => '/',
'domain' => COOKIE_DOMAIN, // leading dot for compatibility or use subdomain
'secure' => true, // or false
'httponly' => true, // or false
'samesite' => 'Strict', // None || Lax || Strict
'secure' => $IS_HTTPS, // secure only over HTTPS
'httponly' => true,
'samesite' => $SAMESITE_POLICY,
));
define('DEL_COOKIE_OPTIONS', array(
'expires' => time() - 60 * 60 * 24 * 10, //10 days
'path' => '/',
'domain' => COOKIE_DOMAIN, // leading dot for compatibility or use subdomain
'secure' => true, // or false
'httponly' => true, // or false
'samesite' => 'Strict', // None || Lax || Strict
'secure' => $IS_HTTPS, // secure only over HTTPS
'httponly' => true,
'samesite' => $SAMESITE_POLICY,
));
define('SK_INC', (int) 0);
?>

View File

@@ -750,4 +750,211 @@ class VAuth
'token' => substr($token, 0, 8) . '...' // Log partial token for debugging
]);
}
/**
* Generate JWT token for API authentication
* @param array $user User data
* @param int|null $expiryTime Optional custom expiry time in seconds
* @return string JWT token
*/
public function generateJWTToken($user, $expiryTime = null)
{
try {
$expiryTime = $expiryTime ?? (24 * 60 * 60); // Default 24 hours
$header = json_encode([
'typ' => 'JWT',
'alg' => 'HS256'
]);
$payload = json_encode([
'user_id' => $user['user_id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role'] ?? 'member',
'iat' => time(),
'exp' => time() + $expiryTime
]);
$headerEncoded = $this->base64UrlEncode($header);
$payloadEncoded = $this->base64UrlEncode($payload);
$jwtSecret = getenv('JWT_SECRET') ?: (defined('JWT_SECRET') ? JWT_SECRET : 'change_this_jwt_secret');
$signature = hash_hmac('sha256', $headerEncoded . '.' . $payloadEncoded, $jwtSecret, true);
$signatureEncoded = $this->base64UrlEncode($signature);
return $headerEncoded . '.' . $payloadEncoded . '.' . $signatureEncoded;
} catch (Exception $e) {
$this->logger->error('JWT generation failed', [
'error' => $e->getMessage(),
'user_id' => $user['user_id'] ?? 'unknown'
]);
throw $e;
}
}
/**
* Validate JWT token
* @param string $token JWT token
* @return array|null User data or null if invalid
*/
public function validateJWTToken($token)
{
try {
$parts = explode('.', $token);
if (count($parts) !== 3) {
$this->logger->logSecurityEvent('Invalid JWT format', ['token' => substr($token, 0, 20) . '...']);
return null;
}
list($headerEncoded, $payloadEncoded, $signatureProvided) = $parts;
// Verify signature
$jwtSecret = getenv('JWT_SECRET') ?: (defined('JWT_SECRET') ? JWT_SECRET : 'change_this_jwt_secret');
$expectedSignature = hash_hmac('sha256', $headerEncoded . '.' . $payloadEncoded, $jwtSecret, true);
$expectedSignatureEncoded = $this->base64UrlEncode($expectedSignature);
if (!hash_equals($expectedSignatureEncoded, $signatureProvided)) {
$this->logger->logSecurityEvent('JWT signature verification failed', []);
return null;
}
// Decode payload
$payload = json_decode($this->base64UrlDecode($payloadEncoded), true);
if (!$payload || !isset($payload['user_id'])) {
$this->logger->logSecurityEvent('Invalid JWT payload', []);
return null;
}
// Check expiration
if (isset($payload['exp']) && $payload['exp'] < time()) {
$this->logger->logSecurityEvent('JWT token expired', ['user_id' => $payload['user_id']]);
return null;
}
// Verify user exists and is active
$sql = "SELECT `user_id`, `username`, `email`, `role`, `status`
FROM `db_users`
WHERE `user_id` = ? AND `status` = 'active'";
$result = $this->db->dbConnection()->Execute($sql, [$payload['user_id']]);
if (!$result || $result->EOF) {
$this->logger->logSecurityEvent('JWT user not found or inactive', ['user_id' => $payload['user_id']]);
return null;
}
$user = $result->fields;
return [
'user_id' => $user['user_id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role']
];
} catch (Exception $e) {
$this->logger->error('JWT validation error', [
'error' => $e->getMessage(),
'token' => substr($token, 0, 20) . '...'
]);
return null;
}
}
/**
* Login with JWT token return (for API clients)
* @param string $identifier Username or email
* @param string $password Password
* @param int|null $expiryTime Optional token expiry time
* @return array Result with token and user data
*/
public function loginWithToken($identifier, $password, $expiryTime = null)
{
try {
// Use regular login to validate credentials
$loginResult = $this->login($identifier, $password, false);
if (!$loginResult['success']) {
return $loginResult;
}
// Generate JWT token
$token = $this->generateJWTToken($loginResult['user'], $expiryTime);
return [
'success' => true,
'message' => 'Login successful',
'token' => $token,
'token_type' => 'Bearer',
'expires_in' => $expiryTime ?? (24 * 60 * 60),
'user' => $loginResult['user']
];
} catch (Exception $e) {
$this->logger->error('Token login error', [
'error' => $e->getMessage(),
'identifier' => $identifier ?? 'unknown'
]);
return ['success' => false, 'message' => 'An error occurred during login'];
}
}
/**
* Authenticate request via Bearer token (for API requests)
* @param string|null $authHeader Authorization header value
* @return array|null User data or null if not authenticated
*/
public function authenticateBearer($authHeader = null)
{
try {
// Get Authorization header if not provided
if ($authHeader === null) {
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
// Apache mod_rewrite workaround
if (empty($authHeader) && function_exists('apache_request_headers')) {
$headers = apache_request_headers();
$authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? '';
}
}
if (empty($authHeader)) {
return null;
}
// Extract Bearer token
if (strpos($authHeader, 'Bearer ') === 0) {
$token = substr($authHeader, 7);
return $this->validateJWTToken($token);
}
return null;
} catch (Exception $e) {
$this->logger->error('Bearer authentication error', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Base64 URL-safe encoding
* @param string $data Data to encode
* @return string Encoded data
*/
private function base64UrlEncode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* Base64 URL-safe decoding
* @param string $data Data to decode
* @return string Decoded data
*/
private function base64UrlDecode($data)
{
return base64_decode(strtr($data, '-_', '+/'));
}
}

View File

@@ -130,7 +130,8 @@ class VDatabase
if (is_array($cfg_vars) and count($cfg_vars) > 0) {
foreach ($cfg_vars as $key => $post_field) {
$query = $db->execute(sprintf("UPDATE `%s` SET `cfg_data` = '%s' WHERE `cfg_name` = '%s' LIMIT 1; ", $db_tbl, $post_field, $key));
// Fixed: Changed cfg_data to cfg_value to match actual database column name
$query = $db->execute(sprintf("UPDATE `%s` SET `cfg_value` = '%s' WHERE `cfg_name` = '%s' LIMIT 1; ", $db_tbl, $post_field, $key));
$count = $db->Affected_Rows() > 0 ? $count + 1 : $count;
if ($_GET['s'] == 'backend-menu-entry1-sub9' and $cfg['activity_logging'] == 1) {
@@ -271,14 +272,15 @@ class VDatabase
$q_get = '`cfg_name` IN ("' . implode('", "', $settings_array) . '")';
$q_result = $db->Execute(sprintf("SELECT `cfg_name`, `cfg_data` FROM `%s` WHERE %s;", $db_table, $q_get));
// Fixed: Changed cfg_data to cfg_value to match actual database column name
$q_result = $db->Execute(sprintf("SELECT `cfg_name`, `cfg_value` FROM `%s` WHERE %s;", $db_table, $q_get));
if ($q_result) {
while (!$q_result->EOF) {
$cfg_name = $q_result->fields['cfg_name'];
$cfg_data = $q_result->fields['cfg_data'];
$cfg[$cfg_name] = $cfg_data;
$smarty->assign($cfg_name, $cfg_data);
$cfg_value = $q_result->fields['cfg_value'];
$cfg[$cfg_name] = $cfg_value;
$smarty->assign($cfg_name, $cfg_value);
@$q_result->MoveNext();
}
}
@@ -458,6 +460,62 @@ class VDatabase
return $rows;
}
/**
* General-purpose query execution method
* Returns array of results instead of ADORecordSet for easier frontend use
*
* @param string $sql SQL query to execute
* @param array $params Optional parameters for prepared statement
* @param int|false $cache_time Optional cache time in seconds (false = no cache)
* @return array Array of associative arrays with query results, empty array on failure
*/
public function execute($sql, $params = [], $cache_time = false)
{
global $db;
$rows = [];
try {
// Validate SQL input
if (empty($sql) || !is_string($sql)) {
throw new InvalidArgumentException('Invalid SQL query');
}
// Ensure params is an array
if (!is_array($params)) {
$params = [$params];
}
// Execute query with or without caching
if ($cache_time && is_numeric($cache_time) && $cache_time > 0) {
$result = $db->CacheExecute($cache_time, $sql, $params);
} else {
$result = $db->Execute($sql, $params);
}
// Check for query errors
if (!$result) {
$logger = VLogger::getInstance();
$logger->logDatabaseError($db->ErrorMsg(), $sql, $params);
return [];
}
// Convert ADORecordSet to plain array
if ($result && !$result->EOF) {
while (!$result->EOF) {
$rows[] = $result->fields;
$result->MoveNext();
}
}
return $rows;
} catch (Exception $e) {
$logger = VLogger::getInstance();
$logger->logDatabaseError($e->getMessage(), $sql ?? '', $params ?? []);
return [];
}
}
/**
* Sanitize input for database queries
* @param mixed $input Input to sanitize

View File

@@ -4047,7 +4047,7 @@ class VFiles
}
/* number format */
public function numFormat($for)
public static function numFormat($for)
{
return VGenerate::nrf($for);
}
@@ -4109,7 +4109,7 @@ class VFiles
}
}
/* duration from seconds */
public function fileDuration($seconds_count)
public static function fileDuration($seconds_count)
{
$delimiter = ':';

View File

@@ -630,7 +630,7 @@ class VGenerate
return $html;
}
/* generate file href html */
public function fileHref($type, $key, $title = '')
public static function fileHref($type, $key, $title = '')
{
require 'f_core/config.href.php';
global $class_database;

View File

@@ -381,7 +381,8 @@ class VMiddleware
$this->sendAPIResponse(['success' => false, 'message' => 'Authentication required'], 401);
} else {
$redirectUrl = urlencode($_SERVER['REQUEST_URI'] ?? '/');
header("Location: /login?redirect={$redirectUrl}");
if (strpos($_SERVER["REQUEST_URI"] ?? "", "/signin") === 0) { http_response_code(401); return; }
header("Location: /signin?redirect={$redirectUrl}");
exit;
}
}

View File

@@ -18,7 +18,7 @@ defined('_ISVALID') or header('Location: /error');
class VRedirect
{
/* do redirect */
public function to($url, $code = 301)
public function to($code = '', $url)
{
session_write_close();
if (headers_sent()) {

View File

@@ -722,7 +722,7 @@ class VUseraccount
}
}
/* get the user profile image */
public function getProfileImage_inc(int $usr_key, $usr_photo = '', int $inc)
public static function getProfileImage_inc(int $usr_key, $usr_photo = '', int $inc)
{
global $cfg, $class_database, $db;

View File

@@ -0,0 +1,393 @@
<?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));
}

View File

@@ -0,0 +1,278 @@
<?php
/**
* Session Helper Functions
* Provides standardized session access across EasyStream
*
* This file resolves conflicts between different session variable names
* used throughout the application.
*/
if (!defined('_ISVALID')) {
die('Direct access is not allowed');
}
/**
* Get current user ID from session
* Handles legacy session variable names for backward compatibility
*
* @return int User ID or 0 if not logged in
*/
function getCurrentUserId() {
// Check modern standard (preferred)
if (isset($_SESSION['USER_ID']) && $_SESSION['USER_ID'] > 0) {
return (int) $_SESSION['USER_ID'];
}
// Check legacy variant 1 (migrate to new standard)
if (isset($_SESSION['usr_id']) && $_SESSION['usr_id'] > 0) {
$_SESSION['USER_ID'] = (int) $_SESSION['usr_id'];
unset($_SESSION['usr_id']);
VLogger::log('info', 'Migrated session variable usr_id to USER_ID', [
'user_id' => $_SESSION['USER_ID']
]);
return (int) $_SESSION['USER_ID'];
}
// Check legacy variant 2 (migrate to new standard)
if (isset($_SESSION['user_id']) && $_SESSION['user_id'] > 0) {
$_SESSION['USER_ID'] = (int) $_SESSION['user_id'];
unset($_SESSION['user_id']);
VLogger::log('info', 'Migrated session variable user_id to USER_ID', [
'user_id' => $_SESSION['USER_ID']
]);
return (int) $_SESSION['USER_ID'];
}
return 0;
}
/**
* Set current user ID in session
* Automatically cleans up legacy session variables
*
* @param int $userId User ID to set
* @return void
*/
function setCurrentUserId($userId) {
$_SESSION['USER_ID'] = (int) $userId;
// Clean up legacy session variables to prevent conflicts
unset($_SESSION['usr_id']);
unset($_SESSION['user_id']);
}
/**
* Check if user is logged in
*
* @return bool True if user is authenticated, false otherwise
*/
function isUserLoggedIn() {
return getCurrentUserId() > 0;
}
/**
* Get current username from session
*
* @return string|null Username or null if not set
*/
function getCurrentUsername() {
return $_SESSION['USER_NAME'] ?? $_SESSION['usr_user'] ?? null;
}
/**
* Get current user email from session
*
* @return string|null Email or null if not set
*/
function getCurrentUserEmail() {
return $_SESSION['USER_EMAIL'] ?? $_SESSION['usr_email'] ?? null;
}
/**
* Get current user key (unique identifier)
*
* @return string|null User key or null if not set
*/
function getCurrentUserKey() {
return $_SESSION['USER_KEY'] ?? null;
}
/**
* Clear user session completely
* Removes all user-related session variables
*
* @return void
*/
function clearUserSession() {
// Standard variables
unset($_SESSION['USER_ID']);
unset($_SESSION['USER_NAME']);
unset($_SESSION['USER_EMAIL']);
unset($_SESSION['USER_KEY']);
// Legacy variables
unset($_SESSION['usr_id']);
unset($_SESSION['user_id']);
unset($_SESSION['usr_user']);
unset($_SESSION['usr_email']);
// Additional user data
unset($_SESSION['usr_verified']);
unset($_SESSION['usr_partner']);
unset($_SESSION['usr_avatar']);
}
/**
* Migrate all session variables to new standard
* Useful for one-time migration during login
*
* @param array $userData User data from database
* @return void
*/
function migrateSessionVariables($userData) {
// Set standard variables
if (isset($userData['usr_id'])) {
setCurrentUserId($userData['usr_id']);
}
if (isset($userData['usr_user'])) {
$_SESSION['USER_NAME'] = $userData['usr_user'];
}
if (isset($userData['usr_email'])) {
$_SESSION['USER_EMAIL'] = $userData['usr_email'];
}
if (isset($userData['usr_key'])) {
$_SESSION['USER_KEY'] = $userData['usr_key'];
}
// Store additional user data if needed
if (isset($userData['usr_verified'])) {
$_SESSION['usr_verified'] = (bool) $userData['usr_verified'];
}
if (isset($userData['usr_partner'])) {
$_SESSION['usr_partner'] = (bool) $userData['usr_partner'];
}
if (isset($userData['usr_avatar'])) {
$_SESSION['usr_avatar'] = $userData['usr_avatar'];
}
VLogger::log('info', 'Session variables migrated to new standard', [
'user_id' => getCurrentUserId()
]);
}
/**
* Get all current user session data
*
* @return array User session data
*/
function getCurrentUserSessionData() {
return [
'user_id' => getCurrentUserId(),
'username' => getCurrentUsername(),
'email' => getCurrentUserEmail(),
'user_key' => getCurrentUserKey(),
'verified' => $_SESSION['usr_verified'] ?? false,
'partner' => $_SESSION['usr_partner'] ?? false,
'avatar' => $_SESSION['usr_avatar'] ?? null,
'is_logged_in' => isUserLoggedIn()
];
}
/**
* Validate session and check for hijacking attempts
*
* @return bool True if session is valid, false if suspicious
*/
function validateUserSession() {
if (!isUserLoggedIn()) {
return true; // No session to validate
}
// Check if user agent changed (possible hijacking)
$currentUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$sessionUserAgent = $_SESSION['USER_AGENT'] ?? '';
if (!empty($sessionUserAgent) && $sessionUserAgent !== $currentUserAgent) {
VLogger::log('warning', 'Session user agent mismatch - possible hijacking', [
'user_id' => getCurrentUserId(),
'session_ua' => substr($sessionUserAgent, 0, 100),
'current_ua' => substr($currentUserAgent, 0, 100)
]);
clearUserSession();
return false;
}
// Check if IP changed (optional strict check)
if (defined('SESSION_IP_CHECK') && SESSION_IP_CHECK === true) {
$currentIp = $_SERVER['REMOTE_ADDR'] ?? '';
$sessionIp = $_SESSION['USER_IP'] ?? '';
if (!empty($sessionIp) && $sessionIp !== $currentIp) {
VLogger::log('warning', 'Session IP mismatch - possible hijacking', [
'user_id' => getCurrentUserId(),
'session_ip' => $sessionIp,
'current_ip' => $currentIp
]);
clearUserSession();
return false;
}
}
return true;
}
/**
* Initialize session security variables
* Call this after successful login
*
* @return void
*/
function initializeSessionSecurity() {
$_SESSION['USER_AGENT'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
$_SESSION['USER_IP'] = $_SERVER['REMOTE_ADDR'] ?? '';
$_SESSION['SESSION_START_TIME'] = time();
}
/**
* Check if session has expired
*
* @param int $timeout Timeout in seconds (default: 1 hour)
* @return bool True if expired, false otherwise
*/
function isSessionExpired($timeout = 3600) {
if (!isset($_SESSION['SESSION_START_TIME'])) {
return false; // No timestamp, can't determine
}
$elapsed = time() - $_SESSION['SESSION_START_TIME'];
if ($elapsed > $timeout) {
VLogger::log('info', 'Session expired', [
'user_id' => getCurrentUserId(),
'elapsed_seconds' => $elapsed
]);
return true;
}
return false;
}
/**
* Refresh session timestamp
* Call periodically to extend session
*
* @return void
*/
function refreshSession() {
if (isUserLoggedIn()) {
$_SESSION['SESSION_START_TIME'] = time();
}
}