Your IP : 216.73.217.78


Current Path : /home/seto/888mirror.pm/includes/
Upload File :
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);">'
         . '&copy; ' . $year . ' ' . h($name) . ' &middot; 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)) . ' &middot; 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;
}