private function processTags(array $tags): array { $versions = []; // Filter und Sortierung der Versionen $validTags = array_filter($tags, function($tag) { return preg_match('/^\d+\.\d+\.\d+$/', $tag['name']); }); usort($validTags, function($a, $b) { return version_compare($b['name'], $a['name']); }); // Gruppierung in Major-Versionen $majorGroups = []; foreach ($validTags as $tag) { $version = $tag['name']; $major = substr($version, 0, strpos($version, '.', strpos($version, '.') + 1)); if (!isset($majorGroups[$major])) { $majorGroups[$major] = [ 'version' => $major, 'minors' => [], 'date' => (new \DateTime($tag['commit']['committer']['date']))->format('Y-m-d') ]; } $majorGroups[$major]['minors'][] = $this->getVersionDetails($version); } return array_values($majorGroups); } private function getVersionDetails(string $version): array { $cacheKey = md5($version); $cacheFile = __DIR__."/../../Resources/data/$cacheKey.json"; if ($this->filesystem->exists($cacheFile)) { return json_decode(file_get_contents($cacheFile), true); } try { $composerJson = $this->fetchComposerJson($version); $details = $this->parseDependencies($composerJson); $this->filesystem->dumpFile($cacheFile, json_encode([ 'version' => $version, 'php' => $details['php'] ?? '-', 'symfony' => $details['symfony/symfony'] ?? $details['symfony/framework-bundle'] ?? '-', 'doctrine' => $details['doctrine/doctrine-bundle'] ?? '-', 'mysql' => $details['doctrine/dbal'] ?? '-', 'changelog' => "https://github.com/contao/contao/releases/tag/$version" ])); return json_decode(file_get_contents($cacheFile), true); } catch (\Exception $e) { // Error Handling return ['version' => $version, 'error' => $e->getMessage()]; } } private function fetchComposerJson(string $version): array { $response = $this->client->request('GET', "https://api.github.com/repos/contao/contao/contents/composer.json?ref=$version", ['headers' => ['Accept' => 'application/vnd.github.v3.raw']] ); if ($response->getStatusCode() !== 200) { throw new \RuntimeException("Failed to fetch composer.json for $version"); } return json_decode($response->getContent(), true); } private function parseDependencies(array $composerJson): array { return array_merge( $composerJson['require'] ?? [], $composerJson['require-dev'] ?? [] ); } private function updateDataIfNeeded(): void { $lockFile = self::CACHE_FILE.'.lock'; $lockHandle = fopen($lockFile, 'w+'); if (!flock($lockHandle, LOCK_EX | LOCK_NB)) { return; // Another process is already updating } try { if ($this->isCacheExpired()) { $tags = $this->fetchGitHubTags(); $processed = $this->processTags($tags); $this->filesystem->dumpFile(self::CACHE_FILE, json_encode($processed)); touch(self::CACHE_FILE); } } finally { flock($lockHandle, LOCK_UN); fclose($lockHandle); $this->filesystem->remove($lockFile); } } private function isCacheExpired(): bool { if (!$this->filesystem->exists(self::CACHE_FILE)) { return true; } $lastModified = filemtime(self::CACHE_FILE); $now = time(); return ($now - $lastModified) > 86400; // 24h } public function getFilteredVersions(string $searchTerm): array { // Sanitize input $searchTerm = substr(preg_replace('/[^a-zA-Z0-9\.\-\s]/', '', $searchTerm), 0, 50); $data = json_decode( file_get_contents(self::CACHE_FILE), true, 512, JSON_THROW_ON_ERROR ); return array_map(function($version) { return array_map('htmlspecialchars', $version); }, array_filter($data, function($v) use ($searchTerm) { return $this->matchSearchTerm($v, $searchTerm); })); } private function matchSearchTerm(array $version, string $term): bool { if (empty($term)) return true; $fields = ['version', 'php', 'symfony', 'doctrine', 'mysql']; foreach ($fields as $field) { if (stripos($version[$field] ?? '', $term) !== false) { return true; } } return false; }