bare-site/html/class/functions.php
cclohmar b93de83519 bare-site v1.0 — zero-dependency PHP micro-framework
7 languages, 3 pages (What/How/Start), 80-line router, 98KB total.

Features:
- No database, no Composer, no build step — just files on disk
- nginx PHP isolation: only index.php executes
- Bot detection with browser heuristic bypass
- Rate limiting, structured weekly logs, geo-location, Telegram alerts
- Multi-language: Accept-Language auto-detect, nav dropdown
- Article sub-page routing via /start/{slug}
- Apache + Caddy config alternatives documented in Start page
- Sample config — no real keys or secrets
2026-06-25 14:12:46 +00:00

332 lines
11 KiB
PHP

<?php
//====================================================
// LOGGING FUNCTIONS
//====================================================
function logging_errors($error_code, $data) {
switch ($error_code) {
case '400': $redirect = 'error/400'; break;
case '401': $redirect = 'error/401'; break;
case '403': $redirect = 'error/403'; break;
case '404': $redirect = 'error/404'; break;
case '405': $redirect = 'error/405'; break;
case '408': $redirect = 'error/408'; break;
case '409': $redirect = 'error/409'; break;
case '410': $redirect = 'error/410'; break;
case '415': $redirect = 'error/415'; break;
case '429': $redirect = 'error/429'; break;
case '500': $redirect = 'error/500'; break;
case '501': $redirect = 'error/501'; break;
case '502': $redirect = 'error/502'; break;
case '503': $redirect = 'error/503'; break;
case '504': $redirect = 'error/504'; break;
default: $redirect = 'error/default'; break;
}
$visitor = !empty($_SESSION['VISITOR']) && is_array($_SESSION['VISITOR']) ? $_SESSION['VISITOR'] : [];
$log_data = [
'correlation_id' => $_SESSION['ID'] ?? 'unknown',
'timestamp' => gmdate(DATE_ATOM),
'error_code' => $error_code,
'log_data' => $data,
'ip' => $visitor['ip'] ?? 'unknown',
'state' => $visitor['state'] ?? 'unknown',
'country' => $visitor['country'] ?? 'unknown'
];
$log_entry = json_encode($log_data, JSON_UNESCAPED_SLASHES);
$week = gmdate('o-\WW');
file_put_contents(__ROOT__ . '/logs/errors_' . $week . '.log', $log_entry . PHP_EOL, FILE_APPEND | LOCK_EX);
return $redirect;
}
function logging_requests() {
$requests_per_second = calculateRequestVelocity($_SESSION['ID']);
$visitor = !empty($_SESSION['VISITOR']) && is_array($_SESSION['VISITOR']) ? $_SESSION['VISITOR'] : [];
if($requests_per_second < BLOCK_THRESHOLD) {
$log_data = [
'correlation_id' => $_SESSION['ID'],
'timestamp' => gmdate(DATE_ATOM),
];
if(isRealVisitor() == true) {
$log_data['type'] = 'visitor';
sendVisitorNotification();
} else {
$log_data['type'] = 'bot';
}
$log_data = array_merge($log_data, [
'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
'uri' => $_SERVER['REQUEST_URI'] ?? 'UNKNOWN',
'ip' => $visitor['ip'] ?? 'unknown',
'state' => $visitor['state'] ?? 'unknown',
'country' => $visitor['country'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN',
'requests_per_second' => $requests_per_second
]);
$log_entry = json_encode($log_data, JSON_UNESCAPED_SLASHES);
$week = gmdate('o-\WW');
file_put_contents(__ROOT__ . '/logs/requests_' . $week . '.log', $log_entry . PHP_EOL, FILE_APPEND | LOCK_EX);
} else {
$redirect = logging_errors("429", $_SERVER['REQUEST_URI']);
header('Location: ' . __URL__ . $redirect);
exit;
}
}
//====================================================
// SECURITY FUNCTIONS
//====================================================
function calculateRequestVelocity($session_id) {
$velocity_file = __ROOT__ . '/logs/velocity.csv';
$now = time();
$csv_line = $session_id . ',' . $now . PHP_EOL;
file_put_contents($velocity_file, $csv_line, FILE_APPEND | LOCK_EX);
$time_window = 1;
$requests_per_second = calculateRecentRequests($session_id, $time_window);
return $requests_per_second;
}
function calculateRecentRequests($session_id, $time_window = 1) {
$velocity_file = __ROOT__ . '/logs/velocity.csv';
$now = time();
$count = 0;
if (!file_exists($velocity_file)) {
return 0;
}
$file = fopen($velocity_file, 'r');
while (($line = fgetcsv($file)) !== FALSE) {
if (count($line) >= 2) {
$stored_session = $line[0];
$timestamp = (int)$line[1];
if ($stored_session === $session_id && ($now - $timestamp) <= $time_window) {
$count++;
}
}
}
fclose($file);
return $count;
}
function calculateTotalSessionRequests($session_id) {
$velocity_file = __ROOT__ . '/logs/velocity.csv';
$count = 0;
if (!file_exists($velocity_file)) {
return 0;
}
$file = fopen($velocity_file, 'r');
while (($line = fgetcsv($file)) !== FALSE) {
if (count($line) >= 2 && $line[0] === $session_id) {
$count++;
}
}
fclose($file);
return $count;
}
function sendVisitorNotification() {
if (!defined('TELEGRAM_BOT_TOKEN') || empty(TELEGRAM_BOT_TOKEN)) return;
if (isset($_SESSION['visitor_notification_sent'])) return;
$total_requests = calculateTotalSessionRequests($_SESSION['ID']);
$visitor = !empty($_SESSION['VISITOR']) && is_array($_SESSION['VISITOR']) ? $_SESSION['VISITOR'] : [];
if ($total_requests >= 2) {
$_SESSION['visitor_notification_sent'] = true;
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN';
$ua_short = strlen($ua) > 80 ? substr($ua, 0, 77) . '...' : $ua;
$referrer = $_SERVER['HTTP_REFERER'] ?? 'direct';
$timestamp = gmdate('Y-m-d H:i:s') . ' UTC';
$message = "👤 *Visitor — " . ($visitor['country'] ?? 'Unknown') . "*\n"
. "• *Time:* $timestamp\n"
. "• *Page:* " . ($_SERVER['REQUEST_URI'] ?? '/') . "\n"
. "• *Referrer:* $referrer\n"
. "• *IP:* " . ($visitor['ip'] ?? 'unknown') . "\n"
. "• *Location:* " . ($visitor['city'] ?? '') . ", " . ($visitor['country'] ?? '') . "\n"
. "• *Pages:* " . $total_requests . "\n"
. "• *Browser:* $ua_short\n"
. "• *Session:* `" . $_SESSION['ID'] . "`";
send2telegram($message);
}
}
function isRealVisitor() {
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$uri = $_SERVER['REQUEST_URI'] ?? '';
$method = $_SERVER['REQUEST_METHOD'] ?? '';
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
$accept_lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
$browser_patterns = [
'/Mozilla\/5\.0.*(Chrome|Firefox|Safari|Edg)\//i',
'/Mozilla\/5\.0.*Mobile.*Safari/i',
'/Mozilla\/5\.0.*(iPhone|Android).*(Chrome|Safari)/i',
];
foreach ($browser_patterns as $pattern) {
if (preg_match($pattern, $user_agent)) return true;
}
foreach (BOT_USER_AGENT_PATTERNS as $pattern) {
if (preg_match($pattern, $user_agent)) return false;
}
if (empty($accept) || empty($accept_lang)) return false;
if (empty($user_agent) || $user_agent === 'UNKNOWN' || strlen($user_agent) < 20) return false;
foreach (BLOCKED_PATHS as $path) {
if (strpos($uri, $path) !== false) return false;
}
$allowed_methods = ['GET', 'POST', 'HEAD'];
if (!in_array($method, $allowed_methods)) return false;
return true;
}
//====================================================
// UTILITY FUNCTIONS
//====================================================
function getVisitor() {
if (!defined('GEO_KEY') || empty(GEO_KEY)) return false;
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ipList = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = trim($ipList[0]);
} elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => 'https://api.ipgeolocation.io/v2/ipgeo?apiKey='.GEO_KEY.'&ip='.$ip,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 2,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$curlError = curl_error($curl);
curl_close($curl);
if ($response === false || $httpCode !== 200) {
$err = $curlError ?: "HTTP $httpCode";
error_log("Geo API failed ($err) for IP $ip");
return false;
}
$data = json_decode($response, true);
if (isset($data)) {
return [
'ip' => $data['ip'],
'city' => $data['location']['city'],
'state' => $data['location']['state_prov'],
'country' => $data['location']['country_name']
];
}
return false;
}
function loadLanguage() {
$language = !empty($_SESSION['SITE']['language']) ? $_SESSION['SITE']['language'] : 'en';
$languageFile = __ROOT__ . "/lng/{$language}/menu.php";
if (file_exists($languageFile)) {
require_once($languageFile);
} else {
require_once(__ROOT__ . "/lng/en/menu.php");
}
}
function generateCorrelationId(): string {
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
function cleanupVelocityLog() {
$velocity_file = __ROOT__ . '/logs/velocity.csv';
$temp_file = __ROOT__ . '/logs/velocity_temp.csv';
$now = time();
$max_age = 86400;
if (!file_exists($velocity_file)) return;
$input = fopen($velocity_file, 'r');
$output = fopen($temp_file, 'w');
while (($line = fgetcsv($input)) !== FALSE) {
if (count($line) >= 2) {
$timestamp = (int)$line[1];
if (($now - $timestamp) <= $max_age) {
fputcsv($output, $line);
}
}
}
fclose($input);
fclose($output);
rename($temp_file, $velocity_file);
}
//====================================================
// TELEGRAM FUNCTIONS — optional, requires TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
//====================================================
function send2telegram($message) {
if (!defined('TELEGRAM_BOT_TOKEN') || !defined('TELEGRAM_CHAT_ID') ||
empty(TELEGRAM_BOT_TOKEN) || empty(TELEGRAM_CHAT_ID)) {
return false;
}
$url = "https://api.telegram.org/bot" . TELEGRAM_BOT_TOKEN . "/sendMessage";
$data = [
'chat_id' => TELEGRAM_CHAT_ID,
'text' => $message,
'parse_mode' => 'Markdown'
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3,
CURLOPT_CONNECTTIMEOUT => 2
]);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error || $httpCode !== 200) {
error_log("Telegram API failed: " . ($error ?: "HTTP $httpCode"));
return false;
}
return true;
}