\s* <', html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8')) ?? '') ) ?? ''); } function developer_feed_items(string $url = DEVELOPER_FEED_URL): array { $handle = curl_init($url); if ($handle === false) { throw new RuntimeException('RSS-Feed konnte nicht initialisiert werden.'); } curl_setopt_array($handle, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Accept: application/rss+xml, application/xml;q=0.9'], CURLOPT_USERAGENT => 'NAK-Reinickendorf-Intranet/1.0', CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_TIMEOUT => 4, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, ]); try { $response = curl_exec($handle); $status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE); $error = curl_error($handle); } finally { curl_close($handle); } if ($response === false || $status < 200 || $status >= 300) { throw new RuntimeException($error !== '' ? $error : 'HTTP ' . $status); } return developer_parse_feed($response); } function developer_parse_feed(string $xml): array { $previous = libxml_use_internal_errors(true); try { $feed = simplexml_load_string($xml, SimpleXMLElement::class, LIBXML_NONET | LIBXML_NOCDATA); } finally { libxml_clear_errors(); libxml_use_internal_errors($previous); } if ($feed === false || !isset($feed->channel->item)) { throw new RuntimeException('Ungültiger RSS-Feed.'); } $items = []; foreach ($feed->channel->item as $item) { $title = developer_feed_plain_text((string) $item->title); if ($title === '') { continue; } $description = developer_feed_plain_text((string) $item->description); $author = trim((string) $item->children('http://purl.org/dc/elements/1.1/')->creator ?: (string) $item->author); if (preg_match('/\(([^)]+)\)\s*$/u', $author, $match) === 1) { $author = trim($match[1]); } else { $author = trim(preg_replace('/<[^>]+>|\S+@\S+/u', '', $author) ?? ''); } $link = trim((string) $item->link); $items[] = [ 'title' => $title, 'description' => $description !== $title ? $description : '', 'url' => filter_var($link, FILTER_VALIDATE_URL) && str_starts_with($link, DEVELOPER_REPO_URL) ? $link : DEVELOPER_REPO_URL, 'author' => $author !== '' ? $author : 'Git', 'timestamp' => strtotime((string) $item->pubDate) ?: 0, ]; } usort($items, static fn(array $a, array $b): int => $b['timestamp'] <=> $a['timestamp']); return $items; } function developer_feed_groups(array $items, ?DateTimeImmutable $now = null): array { $now ??= new DateTimeImmutable(); $today = $now->setTime(0, 0); $week = $today->modify('monday this week'); $month = $today->modify('first day of this month'); $year = $today->setDate((int) $today->format('Y'), 1, 1); $starts = [ 'Heute' => $today, 'Gestern' => $today->modify('-1 day'), 'Diese Woche' => $week, 'Letzte Woche' => $week->modify('-1 week'), 'Diesen Monat' => $month, 'Letzten Monat' => $month->modify('-1 month'), 'Dieses Jahr' => $year, 'Letztes Jahr' => $year->modify('-1 year'), ]; $groups = []; foreach ($items as $item) { $date = (new DateTimeImmutable('@' . (int) $item['timestamp']))->setTimezone($now->getTimezone()); $group = 'Älter'; foreach ($starts as $label => $start) { if ($date >= $start) { $group = $label; break; } } $groups[$group][] = $item; } return $groups; } function developer_feed_icon(string $title): string { $title = strtolower($title); return match (true) { str_contains($title, 'merge pull'), str_starts_with($title, 'merge ') => 'M7 3a3 3 0 1 0-2 2.83v12.34a3 3 0 1 0 2 0V9h4a4 4 0 0 1 4 4v1.17a3 3 0 1 0 2 0V13a6 6 0 0 0-6-6H7V5.83A3 3 0 0 0 7 3Z', preg_match('/\b(fix|fixed|bugfix|hotfix|repair)\b/u', $title) === 1 => 'M14.7 6.3a5 5 0 0 0-6.47 6.47l-5.94 5.94a1 1 0 0 0 0 1.42l1.58 1.58a1 1 0 0 0 1.42 0l5.94-5.94A5 5 0 0 0 17.7 9.3l-3 3-3-3 3-3Z', preg_match('/\b(remove|removed|delete|deleted|revert)\b/u', $title) === 1 => 'M9 3h6l1 2h4v2H4V5h4l1-2Zm-3 6h12l-1 12H7L6 9Zm3 2v8h2v-8H9Zm4 0v8h2v-8h-2Z', preg_match('/\b(refactor|cleanup|clean up|simplif|optimi[sz])\w*/u', $title) === 1 => 'M7 3a3 3 0 1 0-2 2.83v12.34a3 3 0 1 0 2 0V14h3a5 5 0 0 0 5-5V7.83a3 3 0 1 0-2 0V9a3 3 0 0 1-3 3H7V5.83A3 3 0 0 0 7 3Z', preg_match('/\b(doc|docs|readme|wiki|tutorial)\w*/u', $title) === 1 => 'M4 3h7a2 2 0 0 1 2 2v15a3 3 0 0 0-3-3H4V3Zm16 0h-5v14h5V3Zm0 16h-5a1 1 0 0 0-1 1h6v-1Z', preg_match('/\b(add|added|new|create|created|introduce|introduced|implement|implemented)\b/u', $title) === 1 => 'M11 5h2v6h6v2h-6v6h-2v-6H5v-2h6V5Z', default => 'M12 3a4 4 0 0 0-3.87 3H3v2h5.13a4 4 0 0 0 7.74 0H21V6h-5.13A4 4 0 0 0 12 3Zm0 2a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm0 10a4 4 0 0 0-3.87 3H3v2h5.13a4 4 0 0 0 7.74 0H21v-2h-5.13A4 4 0 0 0 12 15Zm0 2a2 2 0 1 1 0 4 2 2 0 0 1 0-4Z', }; }