Sync current dev state
This commit is contained in:
71
api/auth.php
71
api/auth.php
@@ -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
552
api/comments.php
Normal 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
142
api/cors.config.php
Normal 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
368
api/subscriptions.php
Normal 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
581
api/user.php
Normal 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
582
api/videos.php
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user