time() ) { return $cache; } $status = [ 'ok' => false, 'configured' => true, 'current_version' => $current, 'latest_version' => $current, 'update_available' => false, 'channel' => $channel, 'manifest_url' => $manifestUrl, 'error' => '', 'checked_at' => gmdate('c'), 'download_url' => '', 'changelog_url' => '', 'notes' => '', 'fetched_at' => time(), ]; try { $raw = self::fetchManifest($manifestUrl); $decoded = json_decode($raw, true); if (!is_array($decoded)) { throw new \RuntimeException('Manifest JSON is invalid.'); } $release = self::pickReleaseForChannel($decoded, $channel); $latest = trim((string)($release['version'] ?? '')); if ($latest === '') { throw new \RuntimeException('Manifest does not include a version for selected channel.'); } $status['ok'] = true; $status['latest_version'] = $latest; $status['download_url'] = (string)($release['download_url'] ?? ''); $status['changelog_url'] = (string)($release['changelog_url'] ?? ''); $status['notes'] = (string)($release['notes'] ?? ''); $status['update_available'] = version_compare($latest, $current, '>'); } catch (Throwable $e) { $status['error'] = $e->getMessage(); } self::writeCache($status); return $status; } private static function pickReleaseForChannel(array $manifest, string $channel): array { if (isset($manifest['channels']) && is_array($manifest['channels'])) { $channels = $manifest['channels']; if (isset($channels[$channel]) && is_array($channels[$channel])) { return $channels[$channel]; } if (isset($channels['stable']) && is_array($channels['stable'])) { return $channels['stable']; } } return $manifest; } private static function cachePath(): string { return __DIR__ . '/../../storage/update_cache.json'; } private static function readCache(): ?array { try { $db = Database::get(); if ($db) { $stmt = $db->query(" SELECT checked_at, channel, manifest_url, current_version, latest_version, is_update_available, ok, error_text, payload_json FROM ac_update_checks ORDER BY id DESC LIMIT 1 "); $row = $stmt ? $stmt->fetch() : null; if (is_array($row)) { $payload = json_decode((string)($row['payload_json'] ?? ''), true); if (is_array($payload)) { $payload['channel'] = (string)($row['channel'] ?? ($payload['channel'] ?? 'stable')); $payload['manifest_url'] = (string)($row['manifest_url'] ?? ($payload['manifest_url'] ?? '')); $payload['current_version'] = (string)($row['current_version'] ?? ($payload['current_version'] ?? '0.0.0')); $payload['latest_version'] = (string)($row['latest_version'] ?? ($payload['latest_version'] ?? '0.0.0')); $payload['update_available'] = ((int)($row['is_update_available'] ?? 0) === 1); $payload['ok'] = ((int)($row['ok'] ?? 0) === 1); $payload['error'] = (string)($row['error_text'] ?? ($payload['error'] ?? '')); $checkedAt = (string)($row['checked_at'] ?? ''); if ($checkedAt !== '') { $payload['checked_at'] = $checkedAt; $payload['fetched_at'] = strtotime($checkedAt) ?: ($payload['fetched_at'] ?? 0); } return $payload; } } } } catch (Throwable $e) { } $path = self::cachePath(); if (!is_file($path)) { return null; } $raw = @file_get_contents($path); if ($raw === false) { return null; } $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : null; } private static function writeCache(array $data): void { try { $db = Database::get(); if ($db) { $stmt = $db->prepare(" INSERT INTO ac_update_checks (checked_at, channel, manifest_url, current_version, latest_version, is_update_available, ok, error_text, payload_json) VALUES (NOW(), :channel, :manifest_url, :current_version, :latest_version, :is_update_available, :ok, :error_text, :payload_json) "); $stmt->execute([ ':channel' => (string)($data['channel'] ?? 'stable'), ':manifest_url' => (string)($data['manifest_url'] ?? ''), ':current_version' => (string)($data['current_version'] ?? '0.0.0'), ':latest_version' => (string)($data['latest_version'] ?? '0.0.0'), ':is_update_available' => !empty($data['update_available']) ? 1 : 0, ':ok' => !empty($data['ok']) ? 1 : 0, ':error_text' => (string)($data['error'] ?? ''), ':payload_json' => json_encode($data, JSON_UNESCAPED_SLASHES), ]); } } catch (Throwable $e) { } $path = self::cachePath(); @file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } private static function fetchManifest(string $manifestUrl): string { $ctx = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => 10, 'header' => "User-Agent: AudioCore-Updater/1.0\r\nAccept: application/json\r\n", ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $raw = @file_get_contents($manifestUrl, false, $ctx); if (is_string($raw) && $raw !== '') { return $raw; } if (!function_exists('curl_init')) { throw new \RuntimeException('Unable to fetch manifest (file_get_contents failed and cURL is unavailable).'); } $ch = curl_init($manifestUrl); if ($ch === false) { throw new \RuntimeException('Unable to fetch manifest (failed to initialize cURL).'); } curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'User-Agent: AudioCore-Updater/1.0', 'Accept: application/json', ]); $body = curl_exec($ch); $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if (!is_string($body) || $body === '') { $detail = $err !== '' ? $err : ('HTTP ' . $httpCode); throw new \RuntimeException('Unable to fetch manifest via cURL: ' . $detail); } if ($httpCode >= 400) { throw new \RuntimeException('Manifest request failed with HTTP ' . $httpCode . '.'); } return $body; } }