Файл: new_top/core/bootstrap.php
Строк: 360
<?php
/**
* Bootstrap — New Top System
*/
declare(strict_types=1);
// ---- mbstring polyfill (some hostings omit the extension) ----
if (!function_exists('mb_substr')) {
function mb_substr($string, $start, $length = null, $encoding = null)
{
return $length === null ? substr((string)$string, (int)$start) : substr((string)$string, (int)$start, (int)$length);
}
}
if (!function_exists('mb_strlen')) {
function mb_strlen($string, $encoding = null)
{
return strlen((string)$string);
}
}
if (!function_exists('mb_strtolower')) {
function mb_strtolower($string, $encoding = null)
{
return strtolower((string)$string);
}
if (!function_exists('str_contains')) {
function str_contains($haystack, $needle)
{
return $needle === '' || strpos((string)$haystack, (string)$needle) !== false;
}
}
if (!function_exists('str_starts_with')) {
function str_starts_with($haystack, $needle)
{
$needle = (string)$needle;
return $needle === '' || strncmp((string)$haystack, $needle, strlen($needle)) === 0;
}
}
if (!function_exists('str_ends_with')) {
function str_ends_with($haystack, $needle)
{
$needle = (string)$needle;
if ($needle === '') return true;
return substr((string)$haystack, -strlen($needle)) === $needle;
}
}
}
define('ROOT', dirname(__DIR__));
define('CORE', ROOT . '/core');
define('CONFIG_PATH', CORE . '/config/database.php');
define('ASSETS', ROOT . '/assets');
if (!file_exists(CONFIG_PATH)) {
if (is_dir(ROOT . '/install') && strpos($_SERVER['REQUEST_URI'] ?? '', '/install') === false) {
header('Location: /install/');
exit;
}
}
$config = file_exists(CONFIG_PATH) ? require CONFIG_PATH : null;
spl_autoload_register(function (string $class): void {
if (!preg_match('/^[A-Za-z][A-Za-z0-9_]*$/', $class)) {
return;
}
$file = CORE . '/classes/' . $class . '.php';
if (is_file($file)) {
require_once $file;
}
});
$pdo = null;
if ($config) {
try {
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$config['db']['host'],
$config['db']['dbname'],
$config['db']['charset']
);
$pdo = new PDO($dsn, $config['db']['username'], $config['db']['password'], $config['db']['options']);
Database::init($pdo);
} catch (PDOException $e) {
http_response_code(500);
exit('Ошибка подключения к базе данных.');
}
}
if (session_status() === PHP_SESSION_NONE) {
$sessName = $config['security']['session_name'] ?? 'TOPSESSID';
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443);
session_name($sessName);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
// Security headers (HTML responses; image endpoints may override Content-Type)
if (!headers_sent()) {
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
}
function e(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function redirect(string $url): void
{
// Only relative same-site paths — prevent open redirect
if ($url === '' || preg_match('#^(https?:)?//#i', $url) || strpos($url, "n") !== false || strpos($url, "r") !== false) {
$url = '/';
}
if (isset($url[0]) && $url[0] !== '/') {
$url = '/';
}
header('Location: ' . $url);
exit;
}
/**
* Client IP. X-Forwarded-For is used only when security.trust_proxy = true in config.
*/
function client_ip(): string
{
global $config;
$remote = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$trustProxy = !empty($config['security']['trust_proxy']);
if ($trustProxy && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
} else {
$ip = $remote;
}
$ip = preg_replace('/[^0-9a-fA-F:.]/', '', $ip) ?: '0.0.0.0';
if (filter_var($ip, FILTER_VALIDATE_IP) === false) {
return '0.0.0.0';
}
return $ip;
}
function client_ua(): string
{
return mb_substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 250, 'UTF-8');
}
function site_url(): string
{
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443);
$scheme = $https ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
// Host header hardening
$host = preg_replace('/[^a-zA-Z0-9.-:]/', '', $host) ?: 'localhost';
return $scheme . '://' . $host;
}
function csrf_token(): string
{
if (empty($_SESSION['_csrf']) || !is_string($_SESSION['_csrf'])) {
$_SESSION['_csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['_csrf'];
}
function csrf_field(): string
{
return '<input type="hidden" name="_csrf" value="' . e(csrf_token()) . '">';
}
function csrf_verify(): void
{
$sent = $_POST['_csrf'] ?? '';
$ok = is_string($sent) && $sent !== '' && hash_equals(csrf_token(), $sent);
if (!$ok) {
http_response_code(403);
exit('Ошибка CSRF. Обновите страницу и повторите.');
}
}
/** Normalize and validate platform hostname/path for storage (no scheme) */
function sanitize_platform_url(string $raw): ?string
{
$url = trim($raw);
$url = preg_replace('#^https?://#i', '', $url) ?? '';
$url = rtrim($url, '/');
$url = mb_strtolower($url);
if ($url === '' || mb_strlen($url) > 60) {
return null;
}
// Reject credentials, schemes, spaces
if (preg_match('#^(javascript|data|vbscript):#i', $url)) {
return null;
}
if (strpos($url, ' ') !== false || strpos($url, '@') !== false || strpos($url, '\') !== false) {
return null;
}
// Must look like domain.tld or domain.tld/path
if (!preg_match('#^[a-z0-9]([a-z0-9-]*[a-z0-9])?(.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+(/[w-./]*)?$#i', $url)) {
return null;
}
return $url;
}
/** Safe external redirect target from stored platform url */
function external_redirect_url(string $stored): ?string
{
$stored = trim($stored);
if ($stored === '' || preg_match('#^(javascript|data|vbscript):#i', $stored)) {
return null;
}
if (preg_match('#^https?://#i', $stored)) {
$parts = parse_url($stored);
if (!$parts || empty($parts['host'])) {
return null;
}
if (!preg_match('#^[a-z0-9.-]+$#i', $parts['host'])) {
return null;
}
$scheme = strtolower($parts['scheme'] ?? 'http');
if ($scheme !== 'http' && $scheme !== 'https') {
return null;
}
return $scheme . '://' . $parts['host']
. (isset($parts['port']) ? ':' . $parts['port'] : '')
. ($parts['path'] ?? '')
. (isset($parts['query']) ? '?' . $parts['query'] : '');
}
$clean = sanitize_platform_url($stored);
if ($clean === null) {
return null;
}
return 'http://' . $clean;
}
/**
* Simple login throttle: max attempts per IP in a window (session-based).
*/
function login_throttle_check(string $bucket = 'login'): bool
{
$key = '_throttle_' . $bucket;
$now = time();
if (empty($_SESSION[$key]) || !is_array($_SESSION[$key])) {
$_SESSION[$key] = ['n' => 0, 'start' => $now];
}
$t = &$_SESSION[$key];
if ($now - (int)$t['start'] > 900) {
$t = ['n' => 0, 'start' => $now];
}
return (int)$t['n'] < 12;
}
function login_throttle_hit(string $bucket = 'login'): void
{
$key = '_throttle_' . $bucket;
if (empty($_SESSION[$key]) || !is_array($_SESSION[$key])) {
$_SESSION[$key] = ['n' => 0, 'start' => time()];
}
$_SESSION[$key]['n'] = (int)$_SESSION[$key]['n'] + 1;
}
function login_throttle_reset(string $bucket = 'login'): void
{
unset($_SESSION['_throttle_' . $bucket]);
}
/** PWA meta tags for <head> */
function pwa_meta(string $appTitle = 'Top Rating System'): string
{
$t = htmlspecialchars($appTitle, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
return <<<HTML
<meta name="theme-color" content="#070b14">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="{$t}">
<meta name="application-name" content="{$t}">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/icons/icon-192.png">
HTML;
}
/** Register service worker before </body> */
function pwa_register(): string
{
return <<<'HTML'
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(function () {});
});
}
</script>
HTML;
}