File size: 4.5Kb
<?php
/**
* CMS: LaiCMS (v1.0 Edition 2026)
* File: system/api.php
* Description: Защищенный JSON API шлюз с глубокой инспекцией трафика.
*/
declare(strict_types=1);
// 1. Усиленные заголовки безопасности (Security Headers)
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY'); // Защита от Clickjacking
header('Content-Security-Policy: default-src \'none\';'); // API не должен ничего грузить стороннего
require_once 'db.php';
require_once 'functions.php';
// Объект ответа (инициализация)
$response = [
'status' => 'error',
'message' => 'Critical error',
'payload' => []
];
/**
* 2. Rate Limiting (Защита от DOS/Bruteforce)
* Ограничиваем количество запросов к API (например, не более 2 в секунду)
*/
if (isset($_SESSION['last_api_request']) && (microtime(true) - $_SESSION['last_api_request'] < 0.5)) {
http_response_code(429);
echo json_encode(['status' => 'error', 'message' => 'Too Many Requests']);
exit;
}
$_SESSION['last_api_request'] = microtime(true);
// 3. Валидация входного потока
$inputJSON = file_get_contents('php://input');
$input = json_decode($inputJSON, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Malformed JSON']);
exit;
}
// 4. Глубокая проверка авторизации и CSRF
if (empty($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
if (!isset($input['csrf_token']) || !check_csrf((string)$input['csrf_token'])) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Security Token Mismatch']);
exit;
}
$action = (string)($input['action'] ?? '');
try {
switch ($action) {
case 'get_user_info':
// Используем наше защищенное ядро q()
$res = q("SELECT id, username, role, balance FROM users WHERE id = ?", [(int)$_SESSION['user_id']]);
$user = $res->fetch_assoc();
if (!$user) throw new Exception("User not found");
$response['status'] = 'success';
$response['payload'] = $user;
break;
case 'update_user_role':
if (!isAdmin()) throw new Exception("Access Denied: Admin privileges required");
$u_id = (int)($input['user_id'] ?? 0);
$new_role = ($input['role'] === 'admin') ? 'admin' : 'user';
if ($u_id === (int)$_SESSION['user_id']) {
throw new Exception("Self-role modification prohibited");
}
$success = q("UPDATE users SET role = ? WHERE id = ?", [$new_role, $u_id]);
if ($success) {
$response['status'] = 'success';
$response['message'] = "Role for ID $u_id updated to $new_role";
track_online_status(); // Обновляем инфо об админе
} else {
throw new Exception("Database update failed");
}
break;
case 'get_system_stats':
if (!isAdmin()) throw new Exception("Forbidden");
$res = q("SELECT COUNT(id) as total FROM users");
$total = $res->fetch_assoc()['total'] ?? 0;
$response['status'] = 'success';
$response['payload'] = [
'total_users' => (int)$total,
'php_version' => PHP_VERSION,
'load_average' => function_exists('sys_getloadavg') ? sys_getloadavg()[0] : 'N/A'
];
break;
default:
http_response_code(404);
throw new Exception("Action '$action' is not registered");
}
} catch (Throwable $e) {
// В режиме 2026 мы логируем реальную ошибку в файл, а пользователю отдаем только безопасное сообщение
error_log("[API Error] " . $e->getMessage() . " in " . $e->getFile());
$response['message'] = $e->getMessage();
}
// 5. Очистка буфера и вывод
if (ob_get_length()) ob_clean();
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);