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

@@ -15,17 +15,11 @@
define('_ISVALID', true);
// Include CORS configuration
require_once __DIR__ . '/cors.config.php';
// Set JSON content type
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
require_once '../f_core/config.core.php';
@@ -227,17 +221,70 @@ try {
if ($method !== 'GET') {
sendResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$isAuthenticated = $auth->isAuthenticated();
$user = $isAuthenticated ? $auth->getCurrentUser() : null;
sendResponse([
'success' => true,
'authenticated' => $isAuthenticated,
'user' => $user
]);
break;
case 'login_token':
// JWT Token-based login for API clients (no session)
if ($method !== 'POST') {
sendResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = array_merge($_POST, getJsonInput());
$missing = validateRequired($data, ['identifier', 'password']);
if (!empty($missing)) {
sendResponse([
'success' => false,
'message' => 'Username/email and password are required'
], 400);
}
// Optional: specify token expiry time (in seconds)
$expiryTime = isset($data['expires_in']) ? (int)$data['expires_in'] : null;
$result = $auth->loginWithToken($data['identifier'], $data['password'], $expiryTime);
sendResponse($result, $result['success'] ? 200 : 401);
break;
case 'verify_token':
// Verify a JWT token and return user info
if ($method !== 'POST' && $method !== 'GET') {
sendResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
// Get token from Authorization header or request body
$user = $auth->authenticateBearer();
if (!$user) {
// Try getting token from request body
$data = array_merge($_POST, getJsonInput());
$token = $data['token'] ?? '';
if ($token) {
$user = $auth->validateJWTToken($token);
}
}
if (!$user) {
sendResponse(['success' => false, 'message' => 'Invalid or expired token'], 401);
}
sendResponse([
'success' => true,
'valid' => true,
'user' => $user
]);
break;
default:
sendResponse(['success' => false, 'message' => 'Invalid action'], 400);
}

552
api/comments.php Normal file
View File

@@ -0,0 +1,552 @@
<?php
/**
* Comments API Endpoint
* Handles all comment-related operations
*
* Supported Actions:
* - GET list: List comments for a video
* - POST create: Create new comment
* - PUT update: Update comment
* - DELETE delete: Delete comment
* - POST like: Like/unlike comment
* - POST report: Report comment
*/
// Include CORS configuration
require_once __DIR__ . '/cors.config.php';
header('Content-Type: application/json');
// Include core configuration
require_once dirname(__FILE__) . '/../f_core/config.core.php';
// Initialize response
$response = ['success' => false, 'data' => null, 'error' => null];
try {
// Get action from query parameter
$action = isset($_GET['action']) ? $_GET['action'] : null;
$method = $_SERVER['REQUEST_METHOD'];
// Get authenticated user (supports both session and JWT)
$userId = null;
// Try JWT authentication first
$authHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] :
(isset($_SERVER['REDIRECT_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 && isset($_SESSION['USER_ID'])) {
$userId = $_SESSION['USER_ID'];
} elseif (!$userId && isset($_SESSION['usr_id'])) {
$userId = $_SESSION['usr_id'];
}
// Route based on method and action
switch ($method) {
case 'GET':
if (isset($_GET['id'])) {
// Get single comment
handleGetComment($_GET['id'], $userId);
} else {
// List comments
handleListComments($userId);
}
break;
case 'POST':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
switch ($action) {
case 'create':
case null:
handleCreateComment($userId);
break;
case 'like':
handleLikeComment($userId);
break;
case 'report':
handleReportComment($userId);
break;
default:
throw new Exception('Invalid action', 400);
}
break;
case 'PUT':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
handleUpdateComment($userId);
break;
case 'DELETE':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
handleDeleteComment($userId);
break;
default:
throw new Exception('Method not allowed', 405);
}
} catch (Exception $e) {
http_response_code($e->getCode() >= 400 && $e->getCode() < 600 ? $e->getCode() : 500);
$response['error'] = $e->getMessage();
echo json_encode($response);
exit;
}
/**
* List comments for a video
*/
function handleListComments($userId) {
global $class_database, $response;
$fileKey = isset($_GET['file_key']) ? $_GET['file_key'] : null;
if (!$fileKey) {
throw new Exception('file_key parameter is required', 400);
}
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 50;
$offset = ($page - 1) * $limit;
$sort = isset($_GET['sort']) ? $_GET['sort'] : 'recent'; // recent, top, oldest
// Determine sort order
$orderBy = match($sort) {
'top' => 'c.comment_likes DESC, c.comment_date DESC',
'oldest' => 'c.comment_date ASC',
default => 'c.comment_date DESC'
};
// Get total count
$countSql = "SELECT COUNT(*) FROM db_comments WHERE file_key = ? AND parent_id IS NULL";
$total = (int)$class_database->singleFieldValue($countSql, [$fileKey]);
// Get top-level comments (not replies)
$sql = "SELECT c.comment_id, c.usr_id, c.comment_text, c.comment_date,
c.comment_likes, c.parent_id,
u.usr_user, u.usr_dname, u.usr_avatar, u.usr_verified,
(SELECT COUNT(*) FROM db_comments WHERE parent_id = c.comment_id) as reply_count
FROM db_comments c
LEFT JOIN db_users u ON c.usr_id = u.usr_id
WHERE c.file_key = ? AND c.parent_id IS NULL
ORDER BY $orderBy
LIMIT ? OFFSET ?";
$comments = $class_database->execute($sql, [$fileKey, $limit, $offset]);
$commentList = $comments ? $comments->GetArray() : [];
// If user is logged in, check which comments they've liked
if ($userId && !empty($commentList)) {
$commentIds = array_column($commentList, 'comment_id');
$placeholders = implode(',', array_fill(0, count($commentIds), '?'));
$likeSql = "SELECT comment_id FROM db_comment_likes WHERE comment_id IN ($placeholders) AND usr_id = ?";
$likedComments = $class_database->execute($likeSql, array_merge($commentIds, [$userId]));
$likedIds = [];
if ($likedComments) {
while (!$likedComments->EOF) {
$likedIds[] = $likedComments->fields['comment_id'];
$likedComments->MoveNext();
}
}
// Add liked status to comments
foreach ($commentList as &$comment) {
$comment['user_liked'] = in_array($comment['comment_id'], $likedIds);
}
}
$response['success'] = true;
$response['data'] = [
'comments' => $commentList,
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit)
]
];
echo json_encode($response);
}
/**
* Get single comment with replies
*/
function handleGetComment($commentId, $userId) {
global $class_database, $response;
// Get the comment
$sql = "SELECT c.comment_id, c.usr_id, c.file_key, c.comment_text, c.comment_date,
c.comment_likes, c.parent_id,
u.usr_user, u.usr_dname, u.usr_avatar, u.usr_verified
FROM db_comments c
LEFT JOIN db_users u ON c.usr_id = u.usr_id
WHERE c.comment_id = ?";
$result = $class_database->execute($sql, [$commentId]);
if (!$result || $result->RecordCount() === 0) {
throw new Exception('Comment not found', 404);
}
$comment = $result->fields;
// Get replies
$repliesSql = "SELECT c.comment_id, c.usr_id, c.comment_text, c.comment_date,
c.comment_likes, c.parent_id,
u.usr_user, u.usr_dname, u.usr_avatar, u.usr_verified
FROM db_comments c
LEFT JOIN db_users u ON c.usr_id = u.usr_id
WHERE c.parent_id = ?
ORDER BY c.comment_date ASC";
$replies = $class_database->execute($repliesSql, [$commentId]);
$comment['replies'] = $replies ? $replies->GetArray() : [];
// Check if user liked this comment
if ($userId) {
$likeSql = "SELECT 1 FROM db_comment_likes WHERE comment_id = ? AND usr_id = ?";
$liked = $class_database->execute($likeSql, [$commentId, $userId]);
$comment['user_liked'] = $liked && $liked->RecordCount() > 0;
}
$response['success'] = true;
$response['data'] = $comment;
echo json_encode($response);
}
/**
* Create new comment
*/
function handleCreateComment($userId) {
global $class_database, $response;
// Get JSON input
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON input', 400);
}
$fileKey = isset($input['file_key']) ? $input['file_key'] : null;
$commentText = isset($input['comment_text']) ? trim($input['comment_text']) : '';
$parentId = isset($input['parent_id']) ? (int)$input['parent_id'] : null;
// Validate required fields
if (!$fileKey) {
throw new Exception('file_key is required', 400);
}
if (empty($commentText)) {
throw new Exception('Comment text cannot be empty', 400);
}
if (strlen($commentText) > 5000) {
throw new Exception('Comment text too long (max 5000 characters)', 400);
}
// Sanitize comment text
$commentText = VSecurity::sanitize($commentText);
// Check if video exists
$videoCheckSql = "SELECT 1 FROM db_videofiles WHERE file_key = ?";
$videoExists = $class_database->execute($videoCheckSql, [$fileKey]);
if (!$videoExists || $videoExists->RecordCount() === 0) {
throw new Exception('Video not found', 404);
}
// If this is a reply, check if parent comment exists
if ($parentId) {
$parentCheckSql = "SELECT usr_id FROM db_comments WHERE comment_id = ?";
$parentExists = $class_database->execute($parentCheckSql, [$parentId]);
if (!$parentExists || $parentExists->RecordCount() === 0) {
throw new Exception('Parent comment not found', 404);
}
$parentUserId = $parentExists->fields['usr_id'];
}
// Insert comment
$sql = "INSERT INTO db_comments (file_key, usr_id, comment_text, comment_date, parent_id, comment_likes)
VALUES (?, ?, ?, NOW(), ?, 0)";
$result = $class_database->execute($sql, [$fileKey, $userId, $commentText, $parentId]);
if (!$result) {
throw new Exception('Failed to create comment', 500);
}
// Get the inserted comment ID
$commentId = $class_database->Insert_ID();
// If this is a reply, create a notification for the parent comment author
if ($parentId && $parentUserId != $userId) {
$notifSql = "INSERT INTO db_notifications (usr_id, notif_type, notif_from, notif_ref_id, notif_date)
VALUES (?, 'comment_reply', ?, ?, NOW())";
$class_database->execute($notifSql, [$parentUserId, $userId, $commentId]);
}
// Get video owner and create notification
$videoOwnerSql = "SELECT usr_id FROM db_videofiles WHERE file_key = ?";
$videoOwner = $class_database->execute($videoOwnerSql, [$fileKey]);
if ($videoOwner && $videoOwner->fields['usr_id'] != $userId) {
$notifSql = "INSERT INTO db_notifications (usr_id, notif_type, notif_from, notif_ref_id, notif_date)
VALUES (?, 'comment', ?, ?, NOW())";
$class_database->execute($notifSql, [$videoOwner->fields['usr_id'], $userId, $commentId]);
}
// Return the created comment
$getCommentSql = "SELECT c.comment_id, c.usr_id, c.file_key, c.comment_text, c.comment_date,
c.comment_likes, c.parent_id,
u.usr_user, u.usr_dname, u.usr_avatar, u.usr_verified
FROM db_comments c
LEFT JOIN db_users u ON c.usr_id = u.usr_id
WHERE c.comment_id = ?";
$newComment = $class_database->execute($getCommentSql, [$commentId]);
$response['success'] = true;
$response['data'] = $newComment->fields;
echo json_encode($response);
}
/**
* Update comment
*/
function handleUpdateComment($userId) {
global $class_database, $response;
$commentId = isset($_GET['id']) ? (int)$_GET['id'] : null;
if (!$commentId) {
throw new Exception('Comment ID is required', 400);
}
// Verify ownership
$checkSql = "SELECT usr_id FROM db_comments WHERE comment_id = ?";
$checkResult = $class_database->execute($checkSql, [$commentId]);
if (!$checkResult || $checkResult->RecordCount() === 0) {
throw new Exception('Comment not found', 404);
}
if ($checkResult->fields['usr_id'] != $userId) {
throw new Exception('You do not have permission to edit this comment', 403);
}
// Get JSON input
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['comment_text'])) {
throw new Exception('comment_text is required', 400);
}
$commentText = trim($input['comment_text']);
if (empty($commentText)) {
throw new Exception('Comment text cannot be empty', 400);
}
if (strlen($commentText) > 5000) {
throw new Exception('Comment text too long (max 5000 characters)', 400);
}
$commentText = VSecurity::sanitize($commentText);
// Update comment
$sql = "UPDATE db_comments SET comment_text = ?, comment_edited = 1 WHERE comment_id = ?";
$result = $class_database->execute($sql, [$commentText, $commentId]);
if (!$result) {
throw new Exception('Failed to update comment', 500);
}
$response['success'] = true;
$response['data'] = ['message' => 'Comment updated successfully'];
echo json_encode($response);
}
/**
* Delete comment
*/
function handleDeleteComment($userId) {
global $class_database, $response;
$commentId = isset($_GET['id']) ? (int)$_GET['id'] : null;
if (!$commentId) {
throw new Exception('Comment ID is required', 400);
}
// Verify ownership or admin status
$checkSql = "SELECT usr_id FROM db_comments WHERE comment_id = ?";
$checkResult = $class_database->execute($checkSql, [$commentId]);
if (!$checkResult || $checkResult->RecordCount() === 0) {
throw new Exception('Comment not found', 404);
}
// Check if user is the comment owner or has admin role
$isOwner = $checkResult->fields['usr_id'] == $userId;
$isAdmin = VAuth::hasPermission('comments.delete.any');
if (!$isOwner && !$isAdmin) {
throw new Exception('You do not have permission to delete this comment', 403);
}
// Delete comment and its replies
$sql = "DELETE FROM db_comments WHERE comment_id = ? OR parent_id = ?";
$result = $class_database->execute($sql, [$commentId, $commentId]);
if (!$result) {
throw new Exception('Failed to delete comment', 500);
}
$response['success'] = true;
$response['data'] = ['message' => 'Comment deleted successfully'];
echo json_encode($response);
}
/**
* Like/unlike comment
*/
function handleLikeComment($userId) {
global $class_database, $response;
$input = json_decode(file_get_contents('php://input'), true);
$commentId = isset($input['comment_id']) ? (int)$input['comment_id'] : null;
if (!$commentId) {
throw new Exception('comment_id is required', 400);
}
// Check if comment exists
$checkSql = "SELECT usr_id FROM db_comments WHERE comment_id = ?";
$commentExists = $class_database->execute($checkSql, [$commentId]);
if (!$commentExists || $commentExists->RecordCount() === 0) {
throw new Exception('Comment not found', 404);
}
$commentOwnerId = $commentExists->fields['usr_id'];
// Check if already liked
$likeSql = "SELECT 1 FROM db_comment_likes WHERE comment_id = ? AND usr_id = ?";
$existing = $class_database->execute($likeSql, [$commentId, $userId]);
if ($existing && $existing->RecordCount() > 0) {
// Remove like
$deleteSql = "DELETE FROM db_comment_likes WHERE comment_id = ? AND usr_id = ?";
$class_database->execute($deleteSql, [$commentId, $userId]);
// Decrement like count
$updateSql = "UPDATE db_comments SET comment_likes = comment_likes - 1 WHERE comment_id = ?";
$class_database->execute($updateSql, [$commentId]);
$action = 'unliked';
} else {
// Add like
$insertSql = "INSERT INTO db_comment_likes (comment_id, usr_id, like_date) VALUES (?, ?, NOW())";
$class_database->execute($insertSql, [$commentId, $userId]);
// Increment like count
$updateSql = "UPDATE db_comments SET comment_likes = comment_likes + 1 WHERE comment_id = ?";
$class_database->execute($updateSql, [$commentId]);
// Create notification for comment owner
if ($commentOwnerId != $userId) {
$notifSql = "INSERT INTO db_notifications (usr_id, notif_type, notif_from, notif_ref_id, notif_date)
VALUES (?, 'comment_like', ?, ?, NOW())";
$class_database->execute($notifSql, [$commentOwnerId, $userId, $commentId]);
}
$action = 'liked';
}
// Get updated like count
$countSql = "SELECT comment_likes FROM db_comments WHERE comment_id = ?";
$countResult = $class_database->execute($countSql, [$commentId]);
$response['success'] = true;
$response['data'] = [
'action' => $action,
'like_count' => $countResult->fields['comment_likes']
];
echo json_encode($response);
}
/**
* Report comment
*/
function handleReportComment($userId) {
global $class_database, $response;
$input = json_decode(file_get_contents('php://input'), true);
$commentId = isset($input['comment_id']) ? (int)$input['comment_id'] : null;
$reason = isset($input['reason']) ? VSecurity::sanitize($input['reason']) : '';
if (!$commentId) {
throw new Exception('comment_id is required', 400);
}
if (empty($reason)) {
throw new Exception('Reason is required', 400);
}
// Check if comment exists
$checkSql = "SELECT 1 FROM db_comments WHERE comment_id = ?";
$commentExists = $class_database->execute($checkSql, [$commentId]);
if (!$commentExists || $commentExists->RecordCount() === 0) {
throw new Exception('Comment not found', 404);
}
// Check if already reported by this user
$reportCheckSql = "SELECT 1 FROM db_reports WHERE comment_id = ? AND usr_id = ?";
$existing = $class_database->execute($reportCheckSql, [$commentId, $userId]);
if ($existing && $existing->RecordCount() > 0) {
throw new Exception('You have already reported this comment', 400);
}
// Insert report
$sql = "INSERT INTO db_reports (comment_id, usr_id, report_reason, report_date, report_status)
VALUES (?, ?, ?, NOW(), 'pending')";
$result = $class_database->execute($sql, [$commentId, $userId, $reason]);
if (!$result) {
throw new Exception('Failed to submit report', 500);
}
$response['success'] = true;
$response['data'] = ['message' => 'Comment reported successfully'];
echo json_encode($response);
}

142
api/cors.config.php Normal file
View File

@@ -0,0 +1,142 @@
<?php
/**
* CORS Configuration for EasyStream API
*
* This file handles Cross-Origin Resource Sharing (CORS) headers
* for all API endpoints in a secure and centralized manner.
*/
/**
* Set CORS headers based on environment configuration
*
* @param array $options Optional CORS configuration
*/
function setAPICorsHeaders($options = []) {
// Get allowed origins from environment or use defaults
$allowedOrigins = [];
// Check if we're in development or production
$isDevelopment = (defined('_DEVEL') && _DEVEL === true) ||
(isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'development');
if ($isDevelopment) {
// Development: Allow localhost and common development ports
$allowedOrigins = [
'http://localhost',
'http://localhost:3000',
'http://localhost:8080',
'http://127.0.0.1',
'http://127.0.0.1:3000',
'http://127.0.0.1:8080',
];
} else {
// Production: Get from environment variable
if (isset($_ENV['CORS_ALLOWED_ORIGINS'])) {
$allowedOrigins = explode(',', $_ENV['CORS_ALLOWED_ORIGINS']);
} elseif (isset($_SERVER['HTTP_HOST'])) {
// Default to same origin
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$allowedOrigins = [$protocol . '://' . $_SERVER['HTTP_HOST']];
}
}
// Get the origin from the request
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
// Check if origin is allowed
$isAllowedOrigin = false;
foreach ($allowedOrigins as $allowedOrigin) {
$allowedOrigin = trim($allowedOrigin);
if ($origin === $allowedOrigin || fnmatch($allowedOrigin, $origin)) {
$isAllowedOrigin = true;
break;
}
}
// Set CORS headers
if ($isAllowedOrigin) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Access-Control-Allow-Credentials: true');
} elseif ($isDevelopment) {
// In development, be more permissive but log it
header('Access-Control-Allow-Origin: *');
error_log('CORS: Allowing all origins in development mode');
}
// Set other CORS headers
$allowedMethods = isset($options['methods'])
? $options['methods']
: 'GET, POST, PUT, DELETE, OPTIONS';
header('Access-Control-Allow-Methods: ' . $allowedMethods);
$allowedHeaders = isset($options['headers'])
? $options['headers']
: 'Content-Type, Authorization, X-Requested-With, X-CSRF-Token';
header('Access-Control-Allow-Headers: ' . $allowedHeaders);
$maxAge = isset($options['max_age']) ? $options['max_age'] : 86400; // 24 hours
header('Access-Control-Max-Age: ' . $maxAge);
// Expose headers that the client can access
$exposedHeaders = isset($options['expose_headers'])
? $options['expose_headers']
: 'Content-Length, X-JSON';
header('Access-Control-Expose-Headers: ' . $exposedHeaders);
}
/**
* Handle preflight OPTIONS request
*
* @return void
*/
function handleCorsPreflightRequest() {
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
setAPICorsHeaders();
http_response_code(200);
exit;
}
}
/**
* Validate origin for sensitive operations
*
* @return bool True if origin is valid, false otherwise
*/
function validateCorsOrigin() {
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
if (empty($origin)) {
return true; // Same-origin requests don't have an Origin header
}
// Get server's own origin
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$serverOrigin = $protocol . '://' . $_SERVER['HTTP_HOST'];
// Check if it's the same origin
if ($origin === $serverOrigin) {
return true;
}
// Check against allowed origins
$isDevelopment = (defined('_DEVEL') && _DEVEL === true) ||
(isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'development');
if ($isDevelopment) {
// More permissive in development
return strpos($origin, 'localhost') !== false ||
strpos($origin, '127.0.0.1') !== false;
}
// Check environment variable
if (isset($_ENV['CORS_ALLOWED_ORIGINS'])) {
$allowedOrigins = explode(',', $_ENV['CORS_ALLOWED_ORIGINS']);
return in_array($origin, array_map('trim', $allowedOrigins));
}
return false;
}
// Automatically set CORS headers and handle preflight when this file is included
setAPICorsHeaders();
handleCorsPreflightRequest();

368
api/subscriptions.php Normal file
View File

@@ -0,0 +1,368 @@
<?php
/**
* Subscriptions API Endpoint
* Handles subscription-related operations
*
* Supported Actions:
* - GET list: Get user's subscriptions or subscribers
* - POST subscribe: Subscribe to a channel
* - DELETE unsubscribe: Unsubscribe from a channel
* - GET feed: Get activity feed from subscribed channels
*/
// Include CORS configuration
require_once __DIR__ . '/cors.config.php';
header('Content-Type: application/json');
// Include core configuration
require_once dirname(__FILE__) . '/../f_core/config.core.php';
// Initialize response
$response = ['success' => false, 'data' => null, 'error' => null];
try {
// Get action from query parameter
$action = isset($_GET['action']) ? $_GET['action'] : null;
$method = $_SERVER['REQUEST_METHOD'];
// Get authenticated user (supports both session and JWT)
$userId = null;
// Try JWT authentication first
$authHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] :
(isset($_SERVER['REDIRECT_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 && isset($_SESSION['USER_ID'])) {
$userId = $_SESSION['USER_ID'];
} elseif (!$userId && isset($_SESSION['usr_id'])) {
$userId = $_SESSION['usr_id'];
}
if (!$userId) {
throw new Exception('Authentication required', 401);
}
// Route based on method and action
switch ($method) {
case 'GET':
switch ($action) {
case 'list':
case 'subscriptions':
case null:
handleGetSubscriptions($userId);
break;
case 'subscribers':
handleGetSubscribers($userId);
break;
case 'feed':
handleGetFeed($userId);
break;
case 'check':
handleCheckSubscription($userId);
break;
default:
throw new Exception('Invalid action', 400);
}
break;
case 'POST':
handleSubscribe($userId);
break;
case 'DELETE':
handleUnsubscribe($userId);
break;
default:
throw new Exception('Method not allowed', 405);
}
} catch (Exception $e) {
http_response_code($e->getCode() >= 400 && $e->getCode() < 600 ? $e->getCode() : 500);
$response['error'] = $e->getMessage();
echo json_encode($response);
exit;
}
/**
* Get user's subscriptions
*/
function handleGetSubscriptions($userId) {
global $class_database, $response;
$sql = "SELECT u.usr_id, u.usr_user, u.usr_dname, u.usr_avatar,
u.usr_verified, u.usr_partner,
s.sub_date,
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = u.usr_id AND approved = 1 AND privacy = 'public') as video_count,
(SELECT COUNT(*) FROM db_subscriptions WHERE channel_id = u.usr_id) as subscriber_count,
(SELECT MAX(upload_date) FROM db_videofiles WHERE usr_id = u.usr_id AND approved = 1) as last_upload
FROM db_subscriptions s
LEFT JOIN db_users u ON s.channel_id = u.usr_id
WHERE s.usr_id = ?
ORDER BY s.sub_date DESC";
$result = $class_database->execute($sql, [$userId]);
$subscriptions = $result ? $result->GetArray() : [];
$response['success'] = true;
$response['data'] = [
'subscriptions' => $subscriptions,
'total' => count($subscriptions)
];
echo json_encode($response);
}
/**
* Get subscribers for a channel
*/
function handleGetSubscribers($userId) {
global $class_database, $response;
$channelId = isset($_GET['channel_id']) ? (int)$_GET['channel_id'] : $userId;
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 50;
$offset = ($page - 1) * $limit;
// Get total count
$countSql = "SELECT COUNT(*) FROM db_subscriptions WHERE channel_id = ?";
$total = (int)$class_database->singleFieldValue($countSql, [$channelId]);
// Get subscribers
$sql = "SELECT u.usr_id, u.usr_user, u.usr_dname, u.usr_avatar,
u.usr_verified, s.sub_date
FROM db_subscriptions s
LEFT JOIN db_users u ON s.usr_id = u.usr_id
WHERE s.channel_id = ?
ORDER BY s.sub_date DESC
LIMIT ? OFFSET ?";
$result = $class_database->execute($sql, [$channelId, $limit, $offset]);
$response['success'] = true;
$response['data'] = [
'subscribers' => $result ? $result->GetArray() : [],
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit)
]
];
echo json_encode($response);
}
/**
* Get activity feed from subscribed channels
*/
function handleGetFeed($userId) {
global $class_database, $response;
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 20;
$offset = ($page - 1) * $limit;
// Get videos from subscribed channels
$sql = "SELECT v.file_key, v.file_title, v.file_description, v.file_duration,
v.file_views, v.upload_date, v.thumbnail, v.featured,
u.usr_id, u.usr_user, u.usr_dname, u.usr_avatar, u.usr_verified,
(SELECT COUNT(*) FROM db_likes WHERE file_key = v.file_key AND like_type = 'like') as like_count,
(SELECT COUNT(*) FROM db_comments WHERE file_key = v.file_key) as comment_count
FROM db_videofiles v
LEFT JOIN db_users u ON v.usr_id = u.usr_id
INNER JOIN db_subscriptions s ON s.channel_id = v.usr_id
WHERE s.usr_id = ? AND v.approved = 1 AND v.privacy = 'public'
ORDER BY v.upload_date DESC
LIMIT ? OFFSET ?";
$result = $class_database->execute($sql, [$userId, $limit, $offset]);
// Get total count
$countSql = "SELECT COUNT(*)
FROM db_videofiles v
INNER JOIN db_subscriptions s ON s.channel_id = v.usr_id
WHERE s.usr_id = ? AND v.approved = 1 AND v.privacy = 'public'";
$total = (int)$class_database->singleFieldValue($countSql, [$userId]);
$response['success'] = true;
$response['data'] = [
'videos' => $result ? $result->GetArray() : [],
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit)
]
];
echo json_encode($response);
}
/**
* Check if subscribed to a channel
*/
function handleCheckSubscription($userId) {
global $class_database, $response;
$channelId = isset($_GET['channel_id']) ? (int)$_GET['channel_id'] : null;
if (!$channelId) {
throw new Exception('channel_id is required', 400);
}
$sql = "SELECT sub_date FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$result = $class_database->execute($sql, [$userId, $channelId]);
$isSubscribed = $result && $result->RecordCount() > 0;
$subDate = $isSubscribed ? $result->fields['sub_date'] : null;
$response['success'] = true;
$response['data'] = [
'is_subscribed' => $isSubscribed,
'subscribed_since' => $subDate
];
echo json_encode($response);
}
/**
* Subscribe to a channel
*/
function handleSubscribe($userId) {
global $class_database, $response;
// Get JSON input or form data
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
$input = $_POST;
}
$channelId = isset($input['channel_id']) ? (int)$input['channel_id'] : null;
if (!$channelId) {
throw new Exception('channel_id is required', 400);
}
if ($channelId == $userId) {
throw new Exception('Cannot subscribe to yourself', 400);
}
// Check if channel exists
$channelCheckSql = "SELECT usr_user FROM db_users WHERE usr_id = ?";
$channelExists = $class_database->execute($channelCheckSql, [$channelId]);
if (!$channelExists || $channelExists->RecordCount() === 0) {
throw new Exception('Channel not found', 404);
}
// Check if already subscribed
$checkSql = "SELECT 1 FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$existing = $class_database->execute($checkSql, [$userId, $channelId]);
if ($existing && $existing->RecordCount() > 0) {
throw new Exception('Already subscribed to this channel', 400);
}
// Add subscription
$sql = "INSERT INTO db_subscriptions (usr_id, channel_id, sub_date) VALUES (?, ?, NOW())";
$result = $class_database->execute($sql, [$userId, $channelId]);
if (!$result) {
throw new Exception('Failed to subscribe', 500);
}
// Create notification for channel owner
$notifSql = "INSERT INTO db_notifications (usr_id, notif_type, notif_from, notif_date)
VALUES (?, 'subscription', ?, NOW())";
$class_database->execute($notifSql, [$channelId, $userId]);
// Get updated subscriber count
$countSql = "SELECT COUNT(*) as count FROM db_subscriptions WHERE channel_id = ?";
$countResult = $class_database->execute($countSql, [$channelId]);
$subscriberCount = $countResult->fields['count'];
// Log the subscription
VLogger::log('info', 'User subscribed to channel', [
'user_id' => $userId,
'channel_id' => $channelId
]);
$response['success'] = true;
$response['data'] = [
'message' => 'Subscribed successfully',
'channel_name' => $channelExists->fields['usr_user'],
'subscriber_count' => $subscriberCount
];
echo json_encode($response);
}
/**
* Unsubscribe from a channel
*/
function handleUnsubscribe($userId) {
global $class_database, $response;
// Get channel ID from query string or JSON body
$channelId = isset($_GET['channel_id']) ? (int)$_GET['channel_id'] : null;
if (!$channelId) {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
$input = $_POST;
}
$channelId = isset($input['channel_id']) ? (int)$input['channel_id'] : null;
}
if (!$channelId) {
throw new Exception('channel_id is required', 400);
}
// Check if subscribed
$checkSql = "SELECT 1 FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$existing = $class_database->execute($checkSql, [$userId, $channelId]);
if (!$existing || $existing->RecordCount() === 0) {
throw new Exception('Not subscribed to this channel', 400);
}
// Remove subscription
$sql = "DELETE FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$result = $class_database->execute($sql, [$userId, $channelId]);
if (!$result) {
throw new Exception('Failed to unsubscribe', 500);
}
// Get updated subscriber count
$countSql = "SELECT COUNT(*) as count FROM db_subscriptions WHERE channel_id = ?";
$countResult = $class_database->execute($countSql, [$channelId]);
$subscriberCount = $countResult->fields['count'];
// Log the unsubscription
VLogger::log('info', 'User unsubscribed from channel', [
'user_id' => $userId,
'channel_id' => $channelId
]);
$response['success'] = true;
$response['data'] = [
'message' => 'Unsubscribed successfully',
'subscriber_count' => $subscriberCount
];
echo json_encode($response);
}

581
api/user.php Normal file
View File

@@ -0,0 +1,581 @@
<?php
/**
* User API Endpoint
* Handles user profile and account operations
*
* Supported Actions:
* - GET profile: Get user profile
* - PUT update: Update user profile
* - POST avatar: Upload avatar
* - GET stats: Get user statistics
* - GET videos: Get user's videos
*/
// Include CORS configuration
require_once __DIR__ . '/cors.config.php';
header('Content-Type: application/json');
// Include core configuration
require_once dirname(__FILE__) . '/../f_core/config.core.php';
// Initialize response
$response = ['success' => false, 'data' => null, 'error' => null];
try {
// Get action from query parameter
$action = isset($_GET['action']) ? $_GET['action'] : 'profile';
$method = $_SERVER['REQUEST_METHOD'];
// Get authenticated user (supports both session and JWT)
$userId = null;
// Try JWT authentication first
$authHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] :
(isset($_SERVER['REDIRECT_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 && isset($_SESSION['USER_ID'])) {
$userId = $_SESSION['USER_ID'];
} elseif (!$userId && isset($_SESSION['usr_id'])) {
$userId = $_SESSION['usr_id'];
}
// Route based on method and action
switch ($method) {
case 'GET':
switch ($action) {
case 'profile':
case 'me':
handleGetProfile($userId);
break;
case 'stats':
handleGetStats($userId);
break;
case 'videos':
handleGetUserVideos($userId);
break;
case 'subscriptions':
handleGetSubscriptions($userId);
break;
case 'subscribers':
handleGetSubscribers($userId);
break;
default:
// Get another user's public profile
$targetUserId = isset($_GET['id']) ? (int)$_GET['id'] : null;
if ($targetUserId) {
handleGetPublicProfile($targetUserId, $userId);
} else {
handleGetProfile($userId);
}
}
break;
case 'POST':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
switch ($action) {
case 'avatar':
handleUploadAvatar($userId);
break;
case 'subscribe':
handleSubscribe($userId);
break;
case 'unsubscribe':
handleUnsubscribe($userId);
break;
default:
throw new Exception('Invalid action', 400);
}
break;
case 'PUT':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
handleUpdateProfile($userId);
break;
case 'DELETE':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
handleDeleteAccount($userId);
break;
default:
throw new Exception('Method not allowed', 405);
}
} catch (Exception $e) {
http_response_code($e->getCode() >= 400 && $e->getCode() < 600 ? $e->getCode() : 500);
$response['error'] = $e->getMessage();
echo json_encode($response);
exit;
}
/**
* Get current user's profile
*/
function handleGetProfile($userId) {
global $class_database, $response;
if (!$userId) {
throw new Exception('Authentication required', 401);
}
$sql = "SELECT usr_id, usr_user, usr_dname, usr_email, usr_fname, usr_lname,
usr_avatar, usr_about, usr_website, usr_location,
usr_verified, usr_partner, usr_affiliate,
usr_joined, usr_lastlogin, usr_profile_privacy
FROM db_users
WHERE usr_id = ?";
$result = $class_database->execute($sql, [$userId]);
if (!$result || $result->RecordCount() === 0) {
throw new Exception('User not found', 404);
}
$user = $result->fields;
// Get user stats
$statsSql = "SELECT
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND approved = 1) as video_count,
(SELECT COUNT(*) FROM db_subscriptions WHERE channel_id = ?) as subscriber_count,
(SELECT COUNT(*) FROM db_subscriptions WHERE usr_id = ?) as subscription_count,
(SELECT SUM(file_views) FROM db_videofiles WHERE usr_id = ?) as total_views
FROM dual";
$stats = $class_database->execute($statsSql, [$userId, $userId, $userId, $userId]);
if ($stats && $stats->RecordCount() > 0) {
$user['stats'] = [
'videos' => (int)$stats->fields['video_count'],
'subscribers' => (int)$stats->fields['subscriber_count'],
'subscriptions' => (int)$stats->fields['subscription_count'],
'views' => (int)$stats->fields['total_views']
];
}
// Get preferences if they exist
$prefSql = "SELECT * FROM db_user_preferences WHERE usr_id = ?";
$prefs = $class_database->execute($prefSql, [$userId]);
if ($prefs && $prefs->RecordCount() > 0) {
$user['preferences'] = $prefs->fields;
}
$response['success'] = true;
$response['data'] = $user;
echo json_encode($response);
}
/**
* Get another user's public profile
*/
function handleGetPublicProfile($targetUserId, $currentUserId) {
global $class_database, $response;
$sql = "SELECT usr_id, usr_user, usr_dname, usr_avatar, usr_about,
usr_website, usr_location, usr_verified, usr_partner,
usr_joined, usr_profile_privacy
FROM db_users
WHERE usr_id = ?";
$result = $class_database->execute($sql, [$targetUserId]);
if (!$result || $result->RecordCount() === 0) {
throw new Exception('User not found', 404);
}
$user = $result->fields;
// Check privacy settings
if ($user['usr_profile_privacy'] === 'private' && $targetUserId != $currentUserId) {
throw new Exception('This profile is private', 403);
}
// Get public stats
$statsSql = "SELECT
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND approved = 1 AND privacy = 'public') as video_count,
(SELECT COUNT(*) FROM db_subscriptions WHERE channel_id = ?) as subscriber_count,
(SELECT SUM(file_views) FROM db_videofiles WHERE usr_id = ? AND approved = 1) as total_views
FROM dual";
$stats = $class_database->execute($statsSql, [$targetUserId, $targetUserId, $targetUserId]);
if ($stats && $stats->RecordCount() > 0) {
$user['stats'] = [
'videos' => (int)$stats->fields['video_count'],
'subscribers' => (int)$stats->fields['subscriber_count'],
'views' => (int)$stats->fields['total_views']
];
}
// Check if current user is subscribed
if ($currentUserId) {
$subSql = "SELECT 1 FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$subResult = $class_database->execute($subSql, [$currentUserId, $targetUserId]);
$user['is_subscribed'] = $subResult && $subResult->RecordCount() > 0;
}
$response['success'] = true;
$response['data'] = $user;
echo json_encode($response);
}
/**
* Update user profile
*/
function handleUpdateProfile($userId) {
global $class_database, $response;
// Get JSON input
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON input', 400);
}
// Build update query dynamically
$updates = [];
$params = [];
$allowedFields = [
'usr_dname', 'usr_fname', 'usr_lname', 'usr_about',
'usr_website', 'usr_location', 'usr_profile_privacy'
];
foreach ($allowedFields as $field) {
if (isset($input[$field])) {
$updates[] = "$field = ?";
$params[] = VSecurity::sanitize($input[$field]);
}
}
if (empty($updates)) {
throw new Exception('No fields to update', 400);
}
$params[] = $userId;
$sql = "UPDATE db_users SET " . implode(', ', $updates) . " WHERE usr_id = ?";
$result = $class_database->execute($sql, $params);
if (!$result) {
throw new Exception('Failed to update profile', 500);
}
// Log the update
VLogger::log('info', 'Profile updated', ['user_id' => $userId, 'fields' => array_keys($input)]);
$response['success'] = true;
$response['data'] = ['message' => 'Profile updated successfully'];
echo json_encode($response);
}
/**
* Upload avatar
*/
function handleUploadAvatar($userId) {
global $class_database, $response;
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
throw new Exception('No file uploaded or upload error occurred', 400);
}
$file = $_FILES['avatar'];
// Validate file type
$allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
throw new Exception('Invalid file type. Only JPG, PNG, and GIF are allowed', 400);
}
// Validate file size (max 5MB)
if ($file['size'] > 5 * 1024 * 1024) {
throw new Exception('File too large. Maximum size is 5MB', 400);
}
// Create upload directory if it doesn't exist
$uploadDir = dirname(__FILE__) . '/../f_data/data_userfiles/user_profile/' . $userId . '/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Generate unique filename
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = 'avatar_' . time() . '.' . $extension;
$targetPath = $uploadDir . $filename;
// Move uploaded file
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
throw new Exception('Failed to save uploaded file', 500);
}
// Update user avatar in database
$avatarUrl = '/f_data/data_userfiles/user_profile/' . $userId . '/' . $filename;
$sql = "UPDATE db_users SET usr_avatar = ? WHERE usr_id = ?";
$result = $class_database->execute($sql, [$avatarUrl, $userId]);
if (!$result) {
throw new Exception('Failed to update avatar in database', 500);
}
$response['success'] = true;
$response['data'] = [
'message' => 'Avatar uploaded successfully',
'avatar_url' => $avatarUrl
];
echo json_encode($response);
}
/**
* Get user statistics
*/
function handleGetStats($userId) {
global $class_database, $response;
if (!$userId) {
throw new Exception('Authentication required', 401);
}
$sql = "SELECT
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND approved = 1) as total_videos,
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND upload_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)) as videos_last_30_days,
(SELECT SUM(file_views) FROM db_videofiles WHERE usr_id = ?) as total_views,
(SELECT COUNT(*) FROM db_subscriptions WHERE channel_id = ?) as total_subscribers,
(SELECT COUNT(*) FROM db_subscriptions WHERE usr_id = ?) as total_subscriptions,
(SELECT COUNT(*) FROM db_comments WHERE usr_id = ?) as total_comments,
(SELECT COUNT(*) FROM db_likes WHERE usr_id = ?) as total_likes_given
FROM dual";
$result = $class_database->execute($sql, [$userId, $userId, $userId, $userId, $userId, $userId, $userId]);
if (!$result || $result->RecordCount() === 0) {
throw new Exception('Failed to fetch statistics', 500);
}
$response['success'] = true;
$response['data'] = $result->fields;
echo json_encode($response);
}
/**
* Get user's videos
*/
function handleGetUserVideos($userId) {
global $class_database, $response;
$targetUserId = isset($_GET['id']) ? (int)$_GET['id'] : $userId;
if (!$targetUserId) {
throw new Exception('User ID required', 400);
}
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 20;
$offset = ($page - 1) * $limit;
// Privacy filter
$privacyWhere = ($targetUserId == $userId) ? "" : "AND privacy = 'public'";
// Get total count
$countSql = "SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND approved = 1 $privacyWhere";
$total = (int)$class_database->singleFieldValue($countSql, [$targetUserId]);
// Get videos
$sql = "SELECT file_key, file_title, file_description, file_duration,
file_views, privacy, upload_date, thumbnail, featured,
(SELECT COUNT(*) FROM db_likes WHERE file_key = db_videofiles.file_key AND like_type = 'like') as like_count,
(SELECT COUNT(*) FROM db_comments WHERE file_key = db_videofiles.file_key) as comment_count
FROM db_videofiles
WHERE usr_id = ? AND approved = 1 $privacyWhere
ORDER BY upload_date DESC
LIMIT ? OFFSET ?";
$videos = $class_database->execute($sql, [$targetUserId, $limit, $offset]);
$response['success'] = true;
$response['data'] = [
'videos' => $videos ? $videos->GetArray() : [],
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit)
]
];
echo json_encode($response);
}
/**
* Get user's subscriptions
*/
function handleGetSubscriptions($userId) {
global $class_database, $response;
if (!$userId) {
throw new Exception('Authentication required', 401);
}
$sql = "SELECT u.usr_id, u.usr_user, u.usr_dname, u.usr_avatar,
s.sub_date,
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = u.usr_id AND approved = 1) as video_count
FROM db_subscriptions s
LEFT JOIN db_users u ON s.channel_id = u.usr_id
WHERE s.usr_id = ?
ORDER BY s.sub_date DESC";
$result = $class_database->execute($sql, [$userId]);
$response['success'] = true;
$response['data'] = $result ? $result->GetArray() : [];
echo json_encode($response);
}
/**
* Get user's subscribers
*/
function handleGetSubscribers($userId) {
global $class_database, $response;
$targetUserId = isset($_GET['id']) ? (int)$_GET['id'] : $userId;
if (!$targetUserId) {
throw new Exception('User ID required', 400);
}
$sql = "SELECT u.usr_id, u.usr_user, u.usr_dname, u.usr_avatar, s.sub_date
FROM db_subscriptions s
LEFT JOIN db_users u ON s.usr_id = u.usr_id
WHERE s.channel_id = ?
ORDER BY s.sub_date DESC";
$result = $class_database->execute($sql, [$targetUserId]);
$response['success'] = true;
$response['data'] = $result ? $result->GetArray() : [];
echo json_encode($response);
}
/**
* Subscribe to a channel
*/
function handleSubscribe($userId) {
global $class_database, $response;
$input = json_decode(file_get_contents('php://input'), true);
$channelId = isset($input['channel_id']) ? (int)$input['channel_id'] : null;
if (!$channelId) {
throw new Exception('Channel ID is required', 400);
}
if ($channelId == $userId) {
throw new Exception('Cannot subscribe to yourself', 400);
}
// Check if already subscribed
$checkSql = "SELECT 1 FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$existing = $class_database->execute($checkSql, [$userId, $channelId]);
if ($existing && $existing->RecordCount() > 0) {
throw new Exception('Already subscribed', 400);
}
// Add subscription
$sql = "INSERT INTO db_subscriptions (usr_id, channel_id, sub_date) VALUES (?, ?, NOW())";
$result = $class_database->execute($sql, [$userId, $channelId]);
if (!$result) {
throw new Exception('Failed to subscribe', 500);
}
// Create notification for channel owner
$notifSql = "INSERT INTO db_notifications (usr_id, notif_type, notif_from, notif_date)
VALUES (?, 'subscription', ?, NOW())";
$class_database->execute($notifSql, [$channelId, $userId]);
$response['success'] = true;
$response['data'] = ['message' => 'Subscribed successfully'];
echo json_encode($response);
}
/**
* Unsubscribe from a channel
*/
function handleUnsubscribe($userId) {
global $class_database, $response;
$input = json_decode(file_get_contents('php://input'), true);
$channelId = isset($input['channel_id']) ? (int)$input['channel_id'] : null;
if (!$channelId) {
throw new Exception('Channel ID is required', 400);
}
// Remove subscription
$sql = "DELETE FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$result = $class_database->execute($sql, [$userId, $channelId]);
if (!$result) {
throw new Exception('Failed to unsubscribe', 500);
}
$response['success'] = true;
$response['data'] = ['message' => 'Unsubscribed successfully'];
echo json_encode($response);
}
/**
* Delete user account
*/
function handleDeleteAccount($userId) {
global $class_database, $response;
// This is a destructive operation, so we'll just mark the account as deleted
// rather than actually deleting it
$sql = "UPDATE db_users SET usr_status = 'deleted', usr_email = CONCAT('deleted_', usr_id, '@deleted.com')
WHERE usr_id = ?";
$result = $class_database->execute($sql, [$userId]);
if (!$result) {
throw new Exception('Failed to delete account', 500);
}
// Log out the user
VAuth::logout();
$response['success'] = true;
$response['data'] = ['message' => 'Account deleted successfully'];
echo json_encode($response);
}

582
api/videos.php Normal file
View File

@@ -0,0 +1,582 @@
<?php
/**
* Videos API Endpoint
* Handles all video-related operations
*
* Supported Actions:
* - GET list: List videos with pagination and filters
* - GET single: Get single video details
* - POST create: Create/upload new video
* - PUT update: Update video details
* - DELETE delete: Delete video
* - POST like: Like/unlike video
* - POST view: Increment view count
*/
// Include CORS configuration
require_once __DIR__ . '/cors.config.php';
header('Content-Type: application/json');
// Include core configuration
require_once dirname(__FILE__) . '/../f_core/config.core.php';
// Initialize response
$response = ['success' => false, 'data' => null, 'error' => null];
try {
// Get action from query parameter
$action = isset($_GET['action']) ? $_GET['action'] : null;
$method = $_SERVER['REQUEST_METHOD'];
// Get authenticated user (supports both session and JWT)
$userId = null;
// Try JWT authentication first
$authHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] :
(isset($_SERVER['REDIRECT_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 && isset($_SESSION['USER_ID'])) {
$userId = $_SESSION['USER_ID'];
} elseif (!$userId && isset($_SESSION['usr_id'])) {
$userId = $_SESSION['usr_id'];
}
// Route based on method and action
switch ($method) {
case 'GET':
if (isset($_GET['id'])) {
// Get single video
handleGetVideo($_GET['id'], $userId);
} elseif ($action === 'search') {
// Search videos
handleSearchVideos($userId);
} else {
// List videos
handleListVideos($userId);
}
break;
case 'POST':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
switch ($action) {
case 'create':
case 'upload':
handleCreateVideo($userId);
break;
case 'like':
handleLikeVideo($userId);
break;
case 'view':
handleIncrementView($userId);
break;
case 'watch_later':
handleWatchLater($userId);
break;
default:
throw new Exception('Invalid action', 400);
}
break;
case 'PUT':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
handleUpdateVideo($userId);
break;
case 'DELETE':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
handleDeleteVideo($userId);
break;
default:
throw new Exception('Method not allowed', 405);
}
} catch (Exception $e) {
http_response_code($e->getCode() >= 400 && $e->getCode() < 600 ? $e->getCode() : 500);
$response['error'] = $e->getMessage();
echo json_encode($response);
exit;
}
/**
* List videos with pagination and filters
*/
function handleListVideos($userId) {
global $class_database, $response;
// Get pagination parameters
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 20;
$offset = ($page - 1) * $limit;
// Get filter parameters
$category = isset($_GET['category']) ? VSecurity::sanitize($_GET['category']) : null;
$sort = isset($_GET['sort']) ? $_GET['sort'] : 'recent';
$channelId = isset($_GET['channel_id']) ? (int)$_GET['channel_id'] : null;
// Build WHERE clause
$where = ["v.approved = 1"];
$params = [];
if ($category) {
$where[] = "v.file_category = ?";
$params[] = $category;
}
if ($channelId) {
$where[] = "v.usr_id = ?";
$params[] = $channelId;
}
// Add privacy filter (only show public videos unless it's the owner)
if ($userId) {
$where[] = "(v.privacy = 'public' OR v.usr_id = ?)";
$params[] = $userId;
} else {
$where[] = "v.privacy = 'public'";
}
$whereClause = implode(' AND ', $where);
// Build ORDER BY clause
$orderBy = match($sort) {
'popular' => 'v.file_views DESC',
'featured' => 'v.featured DESC, v.file_views DESC',
'recent' => 'v.upload_date DESC',
'oldest' => 'v.upload_date ASC',
'title' => 'v.file_title ASC',
default => 'v.upload_date DESC'
};
// Get total count
$countSql = "SELECT COUNT(*) FROM db_videofiles v WHERE $whereClause";
$total = (int)$class_database->singleFieldValue($countSql, $params);
// Get videos
$sql = "SELECT v.file_key, v.file_title, v.file_description, v.file_name,
v.file_duration, v.file_views, v.privacy, v.upload_date,
v.thumbnail, v.featured, v.file_category,
u.usr_id, u.usr_user, u.usr_dname,
(SELECT COUNT(*) FROM db_likes WHERE file_key = v.file_key AND like_type = 'like') as like_count,
(SELECT COUNT(*) FROM db_comments WHERE file_key = v.file_key) as comment_count
FROM db_videofiles v
LEFT JOIN db_users u ON v.usr_id = u.usr_id
WHERE $whereClause
ORDER BY $orderBy
LIMIT ? OFFSET ?";
$params[] = $limit;
$params[] = $offset;
$videos = $class_database->execute($sql, $params);
// Format response
$response['success'] = true;
$response['data'] = [
'videos' => $videos ? $videos->GetArray() : [],
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit)
]
];
echo json_encode($response);
}
/**
* Get single video details
*/
function handleGetVideo($fileKey, $userId) {
global $class_database, $response;
$sql = "SELECT v.*,
u.usr_id, u.usr_user, u.usr_dname, u.usr_avatar,
(SELECT COUNT(*) FROM db_likes WHERE file_key = v.file_key AND like_type = 'like') as like_count,
(SELECT COUNT(*) FROM db_likes WHERE file_key = v.file_key AND like_type = 'dislike') as dislike_count,
(SELECT COUNT(*) FROM db_comments WHERE file_key = v.file_key) as comment_count,
(SELECT COUNT(*) FROM db_subscriptions WHERE channel_id = v.usr_id) as subscriber_count
FROM db_videofiles v
LEFT JOIN db_users u ON v.usr_id = u.usr_id
WHERE v.file_key = ?";
$result = $class_database->execute($sql, [$fileKey]);
if (!$result || $result->RecordCount() === 0) {
throw new Exception('Video not found', 404);
}
$video = $result->fields;
// Check privacy
if ($video['privacy'] !== 'public' && (!$userId || $userId != $video['usr_id'])) {
throw new Exception('Video not available', 403);
}
// Check if user liked/disliked
if ($userId) {
$likeSql = "SELECT like_type FROM db_likes WHERE file_key = ? AND usr_id = ?";
$likeResult = $class_database->execute($likeSql, [$fileKey, $userId]);
$video['user_like_status'] = $likeResult && $likeResult->RecordCount() > 0 ? $likeResult->fields['like_type'] : null;
// Check if subscribed to channel
$subSql = "SELECT 1 FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$subResult = $class_database->execute($subSql, [$userId, $video['usr_id']]);
$video['user_subscribed'] = $subResult && $subResult->RecordCount() > 0;
}
$response['success'] = true;
$response['data'] = $video;
echo json_encode($response);
}
/**
* Search videos
*/
function handleSearchVideos($userId) {
global $class_database, $response;
$query = isset($_GET['q']) ? VSecurity::sanitize($_GET['q']) : '';
if (strlen($query) < 2) {
throw new Exception('Search query must be at least 2 characters', 400);
}
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 20;
$offset = ($page - 1) * $limit;
$searchTerm = '%' . $query . '%';
$where = "(v.file_title LIKE ? OR v.file_description LIKE ? OR v.file_tags LIKE ?)";
if ($userId) {
$where .= " AND (v.privacy = 'public' OR v.usr_id = ?)";
$params = [$searchTerm, $searchTerm, $searchTerm, $userId];
} else {
$where .= " AND v.privacy = 'public'";
$params = [$searchTerm, $searchTerm, $searchTerm];
}
// Get total count
$countSql = "SELECT COUNT(*) FROM db_videofiles v WHERE v.approved = 1 AND $where";
$total = (int)$class_database->singleFieldValue($countSql, $params);
// Get results
$sql = "SELECT v.file_key, v.file_title, v.file_description, v.file_duration,
v.file_views, v.upload_date, v.thumbnail,
u.usr_id, u.usr_user, u.usr_dname
FROM db_videofiles v
LEFT JOIN db_users u ON v.usr_id = u.usr_id
WHERE v.approved = 1 AND $where
ORDER BY v.file_views DESC, v.upload_date DESC
LIMIT ? OFFSET ?";
$params[] = $limit;
$params[] = $offset;
$videos = $class_database->execute($sql, $params);
$response['success'] = true;
$response['data'] = [
'videos' => $videos ? $videos->GetArray() : [],
'query' => $query,
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit)
]
];
echo json_encode($response);
}
/**
* Create/upload new video
*/
function handleCreateVideo($userId) {
global $class_database, $response;
// Get JSON input
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON input', 400);
}
// Validate required fields
$title = isset($input['title']) ? VSecurity::sanitize($input['title']) : null;
$description = isset($input['description']) ? VSecurity::sanitize($input['description']) : '';
if (!$title) {
throw new Exception('Title is required', 400);
}
// Generate file key
$fileKey = rand(100000, 999999);
// Get optional fields
$privacy = isset($input['privacy']) ? $input['privacy'] : 'public';
$category = isset($input['category']) ? VSecurity::sanitize($input['category']) : null;
$tags = isset($input['tags']) ? VSecurity::sanitize($input['tags']) : null;
// Insert video record
$sql = "INSERT INTO db_videofiles (usr_id, file_key, file_type, file_title, file_description,
privacy, file_category, file_tags, upload_date, approved, file_views)
VALUES (?, ?, 'video', ?, ?, ?, ?, ?, NOW(), 1, 0)";
$result = $class_database->execute($sql, [
$userId, $fileKey, $title, $description, $privacy, $category, $tags
]);
if (!$result) {
throw new Exception('Failed to create video', 500);
}
$response['success'] = true;
$response['data'] = [
'file_key' => $fileKey,
'message' => 'Video created successfully'
];
echo json_encode($response);
}
/**
* Update video details
*/
function handleUpdateVideo($userId) {
global $class_database, $response;
$fileKey = isset($_GET['id']) ? $_GET['id'] : null;
if (!$fileKey) {
throw new Exception('Video ID is required', 400);
}
// Verify ownership
$checkSql = "SELECT usr_id FROM db_videofiles WHERE file_key = ?";
$checkResult = $class_database->execute($checkSql, [$fileKey]);
if (!$checkResult || $checkResult->RecordCount() === 0) {
throw new Exception('Video not found', 404);
}
if ($checkResult->fields['usr_id'] != $userId) {
throw new Exception('You do not have permission to edit this video', 403);
}
// Get JSON input
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON input', 400);
}
// Build update query dynamically
$updates = [];
$params = [];
$allowedFields = ['file_title', 'file_description', 'privacy', 'file_category', 'file_tags', 'thumbnail'];
foreach ($allowedFields as $field) {
if (isset($input[$field])) {
$updates[] = "$field = ?";
$params[] = VSecurity::sanitize($input[$field]);
}
}
if (empty($updates)) {
throw new Exception('No fields to update', 400);
}
$params[] = $fileKey;
$sql = "UPDATE db_videofiles SET " . implode(', ', $updates) . " WHERE file_key = ?";
$result = $class_database->execute($sql, $params);
if (!$result) {
throw new Exception('Failed to update video', 500);
}
$response['success'] = true;
$response['data'] = ['message' => 'Video updated successfully'];
echo json_encode($response);
}
/**
* Delete video
*/
function handleDeleteVideo($userId) {
global $class_database, $response;
$fileKey = isset($_GET['id']) ? $_GET['id'] : null;
if (!$fileKey) {
throw new Exception('Video ID is required', 400);
}
// Verify ownership
$checkSql = "SELECT usr_id, file_name FROM db_videofiles WHERE file_key = ?";
$checkResult = $class_database->execute($checkSql, [$fileKey]);
if (!$checkResult || $checkResult->RecordCount() === 0) {
throw new Exception('Video not found', 404);
}
if ($checkResult->fields['usr_id'] != $userId) {
throw new Exception('You do not have permission to delete this video', 403);
}
// Delete video record
$sql = "DELETE FROM db_videofiles WHERE file_key = ?";
$result = $class_database->execute($sql, [$fileKey]);
if (!$result) {
throw new Exception('Failed to delete video', 500);
}
// TODO: Delete associated files from storage
$response['success'] = true;
$response['data'] = ['message' => 'Video deleted successfully'];
echo json_encode($response);
}
/**
* Like/unlike video
*/
function handleLikeVideo($userId) {
global $class_database, $response;
$input = json_decode(file_get_contents('php://input'), true);
$fileKey = isset($input['file_key']) ? $input['file_key'] : null;
$likeType = isset($input['like_type']) ? $input['like_type'] : 'like'; // 'like' or 'dislike'
if (!$fileKey) {
throw new Exception('Video ID is required', 400);
}
// Check if already liked
$checkSql = "SELECT like_id, like_type FROM db_likes WHERE file_key = ? AND usr_id = ?";
$existing = $class_database->execute($checkSql, [$fileKey, $userId]);
if ($existing && $existing->RecordCount() > 0) {
if ($existing->fields['like_type'] === $likeType) {
// Remove like/dislike
$sql = "DELETE FROM db_likes WHERE file_key = ? AND usr_id = ?";
$class_database->execute($sql, [$fileKey, $userId]);
$action = 'removed';
} else {
// Change like to dislike or vice versa
$sql = "UPDATE db_likes SET like_type = ? WHERE file_key = ? AND usr_id = ?";
$class_database->execute($sql, [$likeType, $fileKey, $userId]);
$action = 'updated';
}
} else {
// Add new like/dislike
$sql = "INSERT INTO db_likes (file_key, usr_id, like_type, like_date) VALUES (?, ?, ?, NOW())";
$class_database->execute($sql, [$fileKey, $userId, $likeType]);
$action = 'added';
}
// Get updated counts
$countSql = "SELECT
SUM(CASE WHEN like_type = 'like' THEN 1 ELSE 0 END) as like_count,
SUM(CASE WHEN like_type = 'dislike' THEN 1 ELSE 0 END) as dislike_count
FROM db_likes WHERE file_key = ?";
$counts = $class_database->execute($countSql, [$fileKey]);
$response['success'] = true;
$response['data'] = [
'action' => $action,
'like_count' => $counts->fields['like_count'] ?? 0,
'dislike_count' => $counts->fields['dislike_count'] ?? 0
];
echo json_encode($response);
}
/**
* Increment view count
*/
function handleIncrementView($userId) {
global $class_database, $response;
$input = json_decode(file_get_contents('php://input'), true);
$fileKey = isset($input['file_key']) ? $input['file_key'] : null;
if (!$fileKey) {
throw new Exception('Video ID is required', 400);
}
// Update view count
$sql = "UPDATE db_videofiles SET file_views = file_views + 1 WHERE file_key = ?";
$class_database->execute($sql, [$fileKey]);
// Log view in activity table if it exists
$activitySql = "INSERT INTO db_user_activity (usr_id, activity_type, file_key, activity_date)
VALUES (?, 'view', ?, NOW())";
$class_database->execute($activitySql, [$userId, $fileKey]);
$response['success'] = true;
$response['data'] = ['message' => 'View recorded'];
echo json_encode($response);
}
/**
* Add/remove video from watch later
*/
function handleWatchLater($userId) {
global $class_database, $response;
$input = json_decode(file_get_contents('php://input'), true);
$fileKey = isset($input['file_key']) ? $input['file_key'] : null;
if (!$fileKey) {
throw new Exception('Video ID is required', 400);
}
// Check if already in watch later
$checkSql = "SELECT 1 FROM db_watchlater WHERE file_key = ? AND usr_id = ?";
$existing = $class_database->execute($checkSql, [$fileKey, $userId]);
if ($existing && $existing->RecordCount() > 0) {
// Remove from watch later
$sql = "DELETE FROM db_watchlater WHERE file_key = ? AND usr_id = ?";
$class_database->execute($sql, [$fileKey, $userId]);
$action = 'removed';
} else {
// Add to watch later
$sql = "INSERT INTO db_watchlater (file_key, usr_id, added_date) VALUES (?, ?, NOW())";
$class_database->execute($sql, [$fileKey, $userId]);
$action = 'added';
}
$response['success'] = true;
$response['data'] = ['action' => $action];
echo json_encode($response);
}