| Current Path : /home/seto/888mirror.pm/includes/ |
| Current File : /home/seto/888mirror.pm/includes/helpers.php |
<?php
function slugify(string $text): string {
$text = mb_strtolower($text, 'UTF-8');
$map = [
'а'=>'a','б'=>'b','в'=>'v','г'=>'g','д'=>'d','е'=>'e','ё'=>'yo',
'ж'=>'zh','з'=>'z','и'=>'i','й'=>'j','к'=>'k','л'=>'l','м'=>'m',
'н'=>'n','о'=>'o','п'=>'p','р'=>'r','с'=>'s','т'=>'t','у'=>'u',
'ф'=>'f','х'=>'h','ц'=>'ts','ч'=>'ch','ш'=>'sh','щ'=>'sch',
'ъ'=>'','ы'=>'y','ь'=>'','э'=>'e','ю'=>'yu','я'=>'ya',
];
$text = strtr($text, $map);
$text = preg_replace('/[^a-z0-9]+/', '-', $text);
return trim($text, '-');
}
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
function json_decode_safe(string $json): array {
$data = json_decode($json, true);
return is_array($data) ? $data : [];
}
function badge_label(string $badge): string {
return match($badge) {
'best' => '★ Best',
'new' => '✦ New',
'free' => '◈ Free',
default => $badge,
};
}
function badge_class(string $badge): string {
return match($badge) {
'best' => 'bg-warning text-dark',
'new' => 'bg-primary',
'free' => 'bg-success',
default => 'bg-secondary',
};
}
function country_name(string $cc): string {
static $map = [
'ae' => 'United Arab Emirates', 'af' => 'Afghanistan', 'al' => 'Albania',
'am' => 'Armenia', 'ao' => 'Angola', 'ar' => 'Argentina',
'at' => 'Austria', 'au' => 'Australia', 'az' => 'Azerbaijan',
'ba' => 'Bosnia & Herzegovina', 'bd' => 'Bangladesh', 'be' => 'Belgium',
'bg' => 'Bulgaria', 'bh' => 'Bahrain', 'bo' => 'Bolivia',
'br' => 'Brazil', 'by' => 'Belarus', 'ca' => 'Canada',
'ch' => 'Switzerland', 'cl' => 'Chile', 'cm' => 'Cameroon',
'cn' => 'China', 'co' => 'Colombia', 'cr' => 'Costa Rica',
'cu' => 'Cuba', 'cy' => 'Cyprus', 'cz' => 'Czech Republic',
'de' => 'Germany', 'dk' => 'Denmark', 'dz' => 'Algeria',
'ec' => 'Ecuador', 'ee' => 'Estonia', 'eg' => 'Egypt',
'es' => 'Spain', 'et' => 'Ethiopia', 'fi' => 'Finland',
'fr' => 'France', 'gb' => 'United Kingdom', 'ge' => 'Georgia',
'gh' => 'Ghana', 'gr' => 'Greece', 'gt' => 'Guatemala',
'hk' => 'Hong Kong', 'hn' => 'Honduras', 'hr' => 'Croatia',
'hu' => 'Hungary', 'id' => 'Indonesia', 'ie' => 'Ireland',
'il' => 'Israel', 'in' => 'India', 'iq' => 'Iraq',
'ir' => 'Iran', 'is' => 'Iceland', 'it' => 'Italy',
'jo' => 'Jordan', 'jp' => 'Japan', 'ke' => 'Kenya',
'kg' => 'Kyrgyzstan', 'kh' => 'Cambodia', 'kr' => 'South Korea',
'kw' => 'Kuwait', 'kz' => 'Kazakhstan', 'la' => 'Laos',
'lb' => 'Lebanon', 'lk' => 'Sri Lanka', 'lt' => 'Lithuania',
'lv' => 'Latvia', 'ly' => 'Libya', 'ma' => 'Morocco',
'md' => 'Moldova', 'me' => 'Montenegro', 'mk' => 'North Macedonia',
'mm' => 'Myanmar', 'mn' => 'Mongolia', 'mt' => 'Malta',
'mx' => 'Mexico', 'my' => 'Malaysia', 'mz' => 'Mozambique',
'ng' => 'Nigeria', 'ni' => 'Nicaragua', 'nl' => 'Netherlands',
'no' => 'Norway', 'np' => 'Nepal', 'nz' => 'New Zealand',
'om' => 'Oman', 'pa' => 'Panama', 'pe' => 'Peru',
'ph' => 'Philippines', 'pk' => 'Pakistan', 'pl' => 'Poland',
'pt' => 'Portugal', 'py' => 'Paraguay', 'qa' => 'Qatar',
'ro' => 'Romania', 'rs' => 'Serbia', 'ru' => 'Russia',
'sa' => 'Saudi Arabia', 'sd' => 'Sudan', 'se' => 'Sweden',
'sg' => 'Singapore', 'si' => 'Slovenia', 'sk' => 'Slovakia',
'sn' => 'Senegal', 'so' => 'Somalia', 'sv' => 'El Salvador',
'sy' => 'Syria', 'th' => 'Thailand', 'tj' => 'Tajikistan',
'tm' => 'Turkmenistan', 'tn' => 'Tunisia', 'tr' => 'Turkey',
'tw' => 'Taiwan', 'tz' => 'Tanzania', 'ua' => 'Ukraine',
'ug' => 'Uganda', 'us' => 'United States', 'uz' => 'Uzbekistan',
'uy' => 'Uruguay', 've' => 'Venezuela', 'vn' => 'Vietnam',
'ye' => 'Yemen', 'za' => 'South Africa', 'zm' => 'Zambia',
'zw' => 'Zimbabwe',
];
return $map[strtolower($cc)] ?? strtoupper($cc);
}
function is_bot_request(): bool {
if ($_SERVER['REQUEST_METHOD'] === 'HEAD') return true;
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
// Fake doubled UA (two Mozilla/5.0 joined by comma — scanner fingerprint)
if (substr_count($ua, 'Mozilla/5.0') >= 2) return true;
// Known scanner/tool UAs
$scanners = ['zgrab', 'masscan', 'nuclei', 'sqlmap', 'nikto', 'nmap',
'dirbuster', 'gobuster', 'wfuzz', 'python-requests',
'libwww-perl', 'go-http-client', 'scrapy'];
$ua_lower = strtolower($ua);
foreach ($scanners as $s) {
if (str_contains($ua_lower, $s)) return true;
}
// Очень старый Chrome (< 100) — реальные пользователи автообновляются, Chrome 100+ с апреля 2022
if (preg_match('/Chrome\/(\d+)/', $ua, $m) && (int)$m[1] < 100) return true;
// Hetzner datacenter IPv6 (2a01:4f8:: и 2a01:4f9::) — серверные IP, не реальные пользователи
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if (str_starts_with($ip, '2a01:4f8:') || str_starts_with($ip, '2a01:4f9:')) return true;
return false;
}
function ip_in_cidr(string $ip, string $cidr): bool {
if (!str_contains($cidr, '/')) {
return $ip === $cidr;
}
[$subnet, $bits] = explode('/', $cidr, 2);
$bits = (int)$bits;
if (str_contains($ip, ':')) {
// IPv6
$ip_bin = inet_pton($ip);
$sub_bin = inet_pton($subnet);
if ($ip_bin === false || $sub_bin === false) return false;
$full_bytes = intdiv($bits, 8);
$rem = $bits % 8;
$mask = str_repeat("\xff", $full_bytes);
if ($rem > 0) $mask .= chr((0xff << (8 - $rem)) & 0xff);
$mask = str_pad($mask, 16, "\x00");
return ($ip_bin & $mask) === ($sub_bin & $mask);
}
// IPv4
$ip_long = ip2long($ip);
$sub_long = ip2long($subnet);
if ($ip_long === false || $sub_long === false) return false;
$mask = $bits > 0 ? (~0 << (32 - $bits)) : 0;
return ($ip_long & $mask) === ($sub_long & $mask);
}
function is_duplicate_click(string $ip, int $brand_id): bool {
$row = db()->prepare("
SELECT 1 FROM stats
WHERE ip = ? AND brand_id = ? AND event_type = 'click'
AND created_at >= datetime('now', '-5 minutes')
LIMIT 1
");
$row->execute([$ip, $brand_id]);
return (bool)$row->fetchColumn();
}
function is_duplicate_visit(string $ip): bool {
$row = db()->prepare("
SELECT 1 FROM stats
WHERE ip = ? AND event_type = 'impression'
AND created_at >= datetime('now', '-5 minutes')
LIMIT 1
");
$row->execute([$ip]);
return (bool)$row->fetchColumn();
}
function referrer_domain(): string {
$ref = $_SERVER['HTTP_REFERER'] ?? '';
if (!$ref) return '';
$host = strtolower(parse_url($ref, PHP_URL_HOST) ?: '');
$host = preg_replace('/^www\./', '', $host);
// убираем self-referral
$own = strtolower(preg_replace('/^www\./', '', $_SERVER['HTTP_HOST'] ?? ''));
return ($host && $host !== $own) ? $host : '';
}
function is_excluded_ip(string $ip): bool {
static $list = null;
if ($list === null) {
$raw = setting('excluded_ips');
$list = array_filter(array_map('trim', explode("\n", $raw)));
}
foreach ($list as $entry) {
if (ip_in_cidr($ip, $entry)) return true;
}
return false;
}
function upload_logo(array $file): string {
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Ошибка загрузки файла.');
}
if ($file['size'] > UPLOAD_MAX_SIZE) {
throw new RuntimeException('Файл слишком большой (макс. 2MB).');
}
$mime = mime_content_type($file['tmp_name']);
if (!in_array($mime, UPLOAD_ALLOWED, true)) {
throw new RuntimeException('Недопустимый формат. Разрешены: JPG, PNG, WEBP.');
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = bin2hex(random_bytes(8)) . '.' . strtolower($ext);
$dest = UPLOAD_DIR . $filename;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
throw new RuntimeException('Не удалось сохранить файл.');
}
return UPLOAD_URL . $filename;
}
function render_footer(): string {
$year = date('Y');
$version = defined('APP_VERSION') ? APP_VERSION : '1.0';
$name = setting('site_name') ?: '888mirror.pm';
return '<footer style="border-top:1px solid rgba(255,255,255,0.08);margin-top:3rem;padding:1rem 0;text-align:center;font-size:0.78rem;color:rgba(255,255,255,0.35);">'
. '© ' . $year . ' ' . h($name) . ' · v' . h($version)
. '</footer>';
}
function admin_nav(string $active = ''): string {
$year = date('Y');
$version = defined('APP_VERSION') ? APP_VERSION : '1.0';
$name = setting('site_name') ?: '888mirror.pm';
$is_admin = (function_exists('auth_role') ? auth_role() : 'admin') === 'admin';
$groups = [
'analytics' => ['Аналитика', '📊', [
'stats' => ['/admin/stats.php', 'Статистика'],
'log' => ['/admin/log.php', 'Лог'],
'conversions' => ['/admin/conversions.php', 'Конверсии'],
]],
'brands_group' => ['Бренды', '🏷️', [
'brands' => ['/admin/brands.php', 'Бренды'],
'partners' => ['/admin/partners.php', 'Партнёрки'],
'redirects' => ['/admin/redirects.php', '↪ Redirects'],
]],
'content' => ['Контент', '🖼️', [
'landings' => ['/admin/landings.php', 'Landings'],
'widget' => ['/admin/widget-settings.php', '🧩 Виджет'],
'languages' => ['/admin/languages.php', 'Языки'],
]],
];
$html = '<script>(function(){var T={"default":"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css","flatly":"https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/flatly/bootstrap.min.css","darkly":"https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/darkly/bootstrap.min.css","cyborg":"https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/cyborg/bootstrap.min.css","minty":"https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/minty/bootstrap.min.css"};var t=localStorage.getItem("admin_theme")||"default";if(t!=="default"&&T[t]){document.documentElement.style.opacity="0";var ls=document.querySelectorAll("link[rel=stylesheet]");for(var i=0;i<ls.length;i++){if(ls[i].href.indexOf("bootstrap")!==-1){ls[i].href=T[t];ls[i].addEventListener("load",function(){document.documentElement.style.opacity="";});setTimeout(function(){document.documentElement.style.opacity="";},400);break;}}}var fl=document.createElement("link");fl.rel="icon";fl.type="image/svg+xml";fl.href="/favicon.svg";document.head.appendChild(fl);var gf=document.createElement("link");gf.rel="stylesheet";gf.href="https://fonts.googleapis.com/css2?family=Russo+One&display=swap";document.head.appendChild(gf);document.addEventListener("DOMContentLoaded",function(){var f=document.createElement("footer");f.style.cssText="border-top:1px solid #dee2e6;margin-top:2rem;padding:.75rem 1rem;text-align:center;font-size:.75rem;color:#adb5bd;";f.innerHTML="© ' . $year . ' ' . addslashes(htmlspecialchars($name, ENT_QUOTES)) . ' · v' . $version . '";document.body.appendChild(f);});})();</script>';
$html .= '<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center gap-2" href="/admin/dashboard.php" style="font-family:\'Russo One\',sans-serif;letter-spacing:.5px"><img src="/favicon.svg" width="28" height="28" style="border-radius:6px">888mirror</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#adminNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="adminNav">
<div class="navbar-nav ms-auto align-items-lg-center">';
$html .= '<a class="nav-link' . ($active === 'dashboard' ? ' active' : '') . '" href="/admin/dashboard.php">Dashboard</a>';
foreach ($groups as $gkey => [$glabel, $gicon, $items]) {
$group_active = false;
foreach ($items as $key => $_) {
if ($key === 'conversions' && !$is_admin) continue;
if ($active === $key) { $group_active = true; break; }
}
$btn_cls = 'nav-link dropdown-toggle' . ($group_active ? ' active' : '');
$html .= '<div class="nav-item dropdown">
<a class="' . $btn_cls . '" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">' . $gicon . ' ' . $glabel . '</a>
<ul class="dropdown-menu dropdown-menu-end">';
foreach ($items as $key => [$url, $label]) {
if ($key === 'conversions' && !$is_admin) continue;
$item_cls = 'dropdown-item' . ($active === $key ? ' active' : '');
$html .= '<li><a class="' . $item_cls . '" href="' . $url . '">' . $label . '</a></li>';
}
$html .= '</ul></div>';
}
$html .= '<a class="nav-link' . ($active === 'settings' ? ' active' : '') . '" href="/admin/settings.php">⚙ Настройки</a>';
$html .= '<a class="nav-link text-danger ms-1" href="/admin/logout.php">Выйти</a>
</div></div></div></nav>';
return $html;
}