'', 'teams' => 'teams', 'groups' => 'groups', 'playoff_rounds' => 'playoff-rounds', 'matches' => 'matches', 'players' => 'players', 'race_stages' => 'race-stages', 'race_standings' => 'race-standings' ]; public const BULK = [ 'event' => 'event', 'teams' => 'teams', 'groups' => 'groups', 'playoff_rounds' => 'playoffRounds', 'matches' => 'matches', 'players' => 'players', 'race_stages' => 'raceStages', 'race_standings' => 'raceStandings' ]; public const LIVE_MIN = 30; public const LIVE_MAX = 600; private const TIMEOUT = 8; private const ERROR_TTL = 60; private const RETRY_MAX = 600; private const MAX_BYTES = 5242880; private const MAX_STALE_BYTES = 2097152; private const MAX_DEPTH = 64; private static array $memo = []; private static array $bulkTried = []; private static bool $degraded = false; private static int $minTtl = 0; public static function base(): string { $base = (string) apply_filters('blyzer_events_api_base', self::BASE); if (!wp_http_validate_url($base) || strtolower((string) wp_parse_url($base, PHP_URL_SCHEME)) !== 'https') { return self::BASE; } return untrailingslashit($base); } public static function ttl(): int { return max(self::$minTtl, (int) get_option('blyzer_cache_ttl', 300)); } public static function setMinTtl(int $seconds): void { self::$minTtl = max(0, $seconds); } public static function needsFetch(int $eventId): bool { if (self::token($eventId) === '' || isset(self::$memo[$eventId . '_event'])) { return false; } if (get_transient('blyzer_down_' . $eventId) !== false || get_transient('blyzer_lock_' . $eventId) !== false) { return false; } return self::ttl() === 0 || get_transient('blyzer_data_' . $eventId . '_event') === false; } public static function liveInterval(): int { return min(self::LIVE_MAX, max(self::LIVE_MIN, (int) get_option('blyzer_live_interval', 60))); } public static function liveTtl(): int { $ttl = self::ttl(); return $ttl === 0 ? 0 : min($ttl, self::liveInterval()); } public static function isLivePayload(array $event, array $matches): bool { if (!empty($event['live']) || ($event['inTime'] ?? '') === 'now') { return true; } foreach ($matches as $_match) { if (is_array($_match) && !empty($_match['live'])) { return true; } } return false; } public static function isLive(int $eventId): bool { return self::isLivePayload(self::fetch($eventId, 'event'), self::peek($eventId, 'matches') ?? []); } private static function peek(int $eventId, string $type): ?array { $key = $eventId . '_' . $type; if (isset(self::$memo[$key])) { return self::$memo[$key]['data']; } $cached = self::normalize(get_transient('blyzer_data_' . $key), $type); return $cached === null ? null : $cached['data']; } public static function bulkSupported(): bool { return (bool) apply_filters('blyzer_events_use_bulk', (int) get_option('blyzer_bulk_unsupported', 0) < time()); } public static function isValidKey(string $key): bool { return (bool) preg_match('/^[A-Za-z0-9]{1,128}$/', $key); } private static function token(int $eventId): string { $token = trim((string) get_post_meta($eventId, '_blyzer_api_key', true)); return preg_match('/^[\x21-\x7E]{1,128}$/', $token) ? $token : ''; } private static function url(string $type): string { $endpoint = self::ENDPOINTS[$type]; return $endpoint === '' ? self::base() : self::base() . '/' . $endpoint; } private static function request(string $url, string $token) { return wp_safe_remote_get($url, [ 'headers' => [ 'X-Auth-Token' => $token, 'Accept' => 'application/json', 'Accept-Encoding' => 'identity' ], 'user-agent' => 'BlyzerEvents/' . BLYZER_EVENTS_VERSION . '; WordPress', 'decompress' => false, 'timeout' => self::TIMEOUT, 'redirection' => 0, 'limit_response_size' => self::MAX_BYTES ]); } public static function probe(string $key): array { if (!self::isValidKey($key)) { return ['code' => 0, 'event' => []]; } $response = self::request(self::url('event'), $key); if (is_wp_error($response)) { return ['code' => 0, 'event' => []]; } $code = (int) wp_remote_retrieve_response_code($response); $event = []; if ($code >= 200 && $code < 300) { $json = json_decode((string) wp_remote_retrieve_body($response), true, self::MAX_DEPTH); $payload = self::normalize(is_array($json) ? ['data' => $json['data'] ?? null, 'meta' => []] : null, 'event'); $event = $payload === null ? [] : $payload['data']; } return ['code' => $code, 'event' => $event]; } public static function status(int $eventId): ?int { if (self::token($eventId) === '') { return null; } $cached = get_transient('blyzer_api_' . $eventId); return $cached === false ? null : (int) $cached; } public static function pingCached(int $eventId): ?int { $code = self::status($eventId); return $code ?? self::ping($eventId); } public static function ping(int $eventId): ?int { $token = self::token($eventId); if ($token === '') { return null; } delete_transient('blyzer_down_' . $eventId); delete_transient('blyzer_err_' . $eventId . '_event'); unset(self::$memo[$eventId . '_event']); self::load($eventId, 'event', true); return self::status($eventId); } private static function rememberStatus(int $eventId, int $code): void { set_transient('blyzer_api_' . $eventId, $code > 0 ? $code : 500, DAY_IN_SECONDS); } public static function fetch(int $eventId, string $type): array { return self::load($eventId, $type)['data']; } public static function meta(int $eventId, string $type): array { return self::load($eventId, $type)['meta']; } public static function load(int $eventId, string $type, bool $force = false): array { $empty = ['data' => [], 'meta' => []]; if (!isset(self::ENDPOINTS[$type])) { return $empty; } $memoKey = $eventId . '_' . $type; if (!$force && isset(self::$memo[$memoKey])) { return self::$memo[$memoKey]; } $token = self::token($eventId); if ($token === '') { return self::$memo[$memoKey] = $empty; } $ttl = self::ttl(); $tkey = 'blyzer_data_' . $eventId . '_' . $type; if ($ttl > 0 && !$force) { $cached = self::normalize(get_transient($tkey), $type); if ($cached !== null) { return self::$memo[$memoKey] = $cached; } } if (!$force && (get_transient('blyzer_down_' . $eventId) !== false || get_transient('blyzer_err_' . $memoKey) !== false)) { return self::$memo[$memoKey] = self::fallback($eventId, $type); } if (!$force && get_transient('blyzer_lock_' . $eventId) !== false) { $stale = self::stale($eventId, $type); if ($stale !== null) { self::$degraded = true; return self::$memo[$memoKey] = $stale; } } if (!$force && !isset(self::$bulkTried[$eventId]) && self::bulkSupported()) { self::$bulkTried[$eventId] = true; $loaded = self::loadBulk($eventId, $token); if (isset(self::$memo[$memoKey])) { return self::$memo[$memoKey]; } if ($loaded === false) { return self::$memo[$memoKey] = self::fallback($eventId, $type); } } $result = self::call($eventId, self::url($type), $token); $code = $result['code']; if ($type === 'event' || $code < 200 || $code >= 300) { self::rememberStatus($eventId, $code); } if ($code >= 300 && $code < 500 && $code !== 429) { set_transient('blyzer_err_' . $memoKey, 1, self::ERROR_TTL); } if ($code < 200 || $code >= 300) { return self::$memo[$memoKey] = self::fallback($eventId, $type); } $json = $result['json']; $payload = self::normalize(is_array($json) ? ['data' => $json['data'] ?? null, 'meta' => $json['meta'] ?? []] : null, $type); if ($payload === null) { set_transient('blyzer_err_' . $memoKey, 1, self::ERROR_TTL); return self::$memo[$memoKey] = self::fallback($eventId, $type); } $live = self::isLivePayload( $type === 'event' ? $payload['data'] : (self::peek($eventId, 'event') ?? []), $type === 'matches' ? $payload['data'] : (self::peek($eventId, 'matches') ?? []) ); return self::$memo[$memoKey] = self::store($eventId, $type, $payload, $live ? self::liveTtl() : $ttl); } private static function store(int $eventId, string $type, array $payload, int $ttl): array { $key = $eventId . '_' . $type; if ($ttl > 0) { set_transient('blyzer_data_' . $key, $payload, $ttl); } $size = strlen((string) wp_json_encode($payload)); if ($size > 0 && $size <= self::MAX_STALE_BYTES) { update_option('blyzer_stale_' . $key, $payload, false); } else { delete_option('blyzer_stale_' . $key); } return $payload; } private static function loadBulk(int $eventId, string $token): ?bool { $result = self::call($eventId, self::base() . '/all', $token); $code = $result['code']; $json = $result['json']; if ($code === 404 && self::errorCode($json) !== 'eventNotFound') { update_option('blyzer_bulk_unsupported', time() + DAY_IN_SECONDS, false); return null; } self::rememberStatus($eventId, $code); $data = is_array($json) && is_array($json['data'] ?? null) ? $json['data'] : null; if ($code < 200 || $code >= 300 || $data === null || TemplateEngine::isList($data)) { if ($code !== 0 && $code !== 429 && $code < 500) { foreach (array_keys(self::ENDPOINTS) as $_type) { set_transient('blyzer_err_' . $eventId . '_' . $_type, 1, self::ERROR_TTL); } } return false; } $meta = is_array($json['meta'] ?? null) ? $json['meta'] : []; $payloads = []; foreach (self::BULK as $_type => $_key) { $payload = self::normalize(['data' => $data[$_key] ?? null, 'meta' => $meta], $_type); if ($payload !== null) { $payloads[$_type] = $payload; } } if (!isset($payloads['event']) || $payloads['event']['data'] === []) { foreach (array_keys(self::ENDPOINTS) as $_type) { set_transient('blyzer_err_' . $eventId . '_' . $_type, 1, self::ERROR_TTL); } return false; } $live = self::isLivePayload($payloads['event']['data'], $payloads['matches']['data'] ?? []); $ttl = $live ? self::liveTtl() : self::ttl(); foreach ($payloads as $_type => $_payload) { self::$memo[$eventId . '_' . $_type] = self::store($eventId, $_type, $_payload, $ttl); } return true; } private static function call(int $eventId, string $url, string $token): array { set_transient('blyzer_lock_' . $eventId, 1, self::TIMEOUT + 2); $response = self::request($url, $token); delete_transient('blyzer_lock_' . $eventId); if (is_wp_error($response)) { set_transient('blyzer_down_' . $eventId, 1, self::ERROR_TTL); return ['code' => 0, 'json' => null]; } $code = (int) wp_remote_retrieve_response_code($response); if ($code === 429) { set_transient('blyzer_down_' . $eventId, 1, self::retryAfter($response)); } elseif ($code >= 500) { set_transient('blyzer_down_' . $eventId, 1, self::ERROR_TTL); } if ($code < 200 || $code >= 300) { $json = $code === 404 ? json_decode((string) wp_remote_retrieve_body($response), true, 8) : null; return ['code' => $code, 'json' => is_array($json) ? $json : null]; } $json = json_decode((string) wp_remote_retrieve_body($response), true, self::MAX_DEPTH); return ['code' => $code, 'json' => is_array($json) ? $json : null]; } private static function retryAfter($response): int { $header = trim((string) wp_remote_retrieve_header($response, 'retry-after')); if ($header !== '' && ctype_digit($header)) { $seconds = (int) $header; } elseif ($header !== '' && ($time = strtotime($header)) !== false) { $seconds = $time - time(); } else { $seconds = self::ERROR_TTL; } return min(self::RETRY_MAX, max(1, $seconds)); } private static function errorCode($json): string { $error = is_array($json) && is_array($json['errors'][0] ?? null) ? $json['errors'][0] : []; return is_string($error['code'] ?? null) ? $error['code'] : ''; } public static function degraded(): bool { return self::$degraded; } private static function fallback(int $eventId, string $type): array { self::$degraded = true; return self::stale($eventId, $type) ?? ['data' => [], 'meta' => []]; } private static function stale(int $eventId, string $type): ?array { return self::normalize(get_option('blyzer_stale_' . $eventId . '_' . $type, null), $type); } private static function normalize($payload, string $type): ?array { if (!is_array($payload)) { return null; } if (!array_key_exists('data', $payload) && TemplateEngine::isList($payload)) { $payload = ['data' => $payload, 'meta' => []]; } $data = $payload['data'] ?? null; $meta = is_array($payload['meta'] ?? null) ? $payload['meta'] : []; if (!is_array($data)) { return null; } if ($type === 'event') { if ($data !== [] && TemplateEngine::isList($data)) { return null; } } elseif (!TemplateEngine::isList($data)) { return null; } return ['data' => $data, 'meta' => $meta]; } public static function invalidateDataCache(int $eventId): void { foreach (array_keys(self::ENDPOINTS) as $_endpoint) { delete_transient('blyzer_data_' . $eventId . '_' . $_endpoint); delete_transient('blyzer_err_' . $eventId . '_' . $_endpoint); unset(self::$memo[$eventId . '_' . $_endpoint]); } unset(self::$bulkTried[$eventId]); delete_transient('blyzer_down_' . $eventId); delete_transient('blyzer_lock_' . $eventId); } public static function deleteAll(int $eventId): void { self::invalidateDataCache($eventId); foreach (array_keys(self::ENDPOINTS) as $_endpoint) { delete_option('blyzer_stale_' . $eventId . '_' . $_endpoint); } delete_transient('blyzer_api_' . $eventId); } }