<?php
 class vB_Api_WidgetData_RSSFeed extends vB_Api { private function parseRssOrAtomFeed(SimpleXMLElement $xml, string $url) : array { $is_atom = isset($xml->entry); $is_rss = isset($xml->channel->item); if ($is_atom) { return $this->mapAtomFeed($xml, $url); } else if ($is_rss) { return $this->mapRssFeed($xml, $url); } else { return $this->mapUnknownFeed($xml, $url); } } private function mapUnknownFeed(SimpleXMLElement $vbRss, string $url) : array { return []; } private function mapAtomFeed(SimpleXMLElement $xml, string $url) : array { $strUtil = vB::getString(); $hooks = vB::getHooks(); $feed = []; foreach ($xml->entry AS $__entry) { $__timestamp = strtotime($__entry->updated); $__feeditem = [ 'title' => $this->stringifyAndConvert($strUtil, $__entry->title), 'description' => $this->stringifyAndConvert($strUtil, ($__entry->summary ?? $__entry->content ?? '')), 'url' => $this->stringifyAndConvert($strUtil, $this->get_atom_link($__entry)), 'timestamp' => $__timestamp, ]; $hooks->invoke('hookRssFeedMapItem', [ 'url' => $url, 'type' => 'atom', 'in_item' => $__entry, 'out_item' => &$__feeditem, ]); $feed[] = $__feeditem; } return $feed; } private function get_atom_link($entry) : string { $links = $entry->link ?? null; if (empty($links)) { return ''; } else { foreach ($links AS $__link) { $__rel = $__link['rel'] ?? ''; if (empty($__rel) OR $__rel == 'alternate') { return (string) ($__link['href'] ?? ''); } } } return ''; } private function mapRssFeed(SimpleXMLElement $xml, string $url) : array { $strUtil = vB::getString(); $hooks = vB::getHooks(); $feed = []; foreach ($xml->channel->item AS $__item) { $__timestamp = strtotime((string) ($__item->{'dc:date'} ?? $__item->pubDate ?? '')); $__feeditem = [ 'title' => $this->stringifyAndConvert($strUtil, $__item->title ?? ''), 'description' => $this->stringifyAndConvert($strUtil, $__item->description ?? ''), 'url' => $this->stringifyAndConvert($strUtil, $__item->link ?? ''), 'timestamp' => $__timestamp, ]; $hooks->invoke('hookRssFeedMapItem', [ 'url' => $url, 'type' => 'rss', 'in_item' => $__item, 'out_item' => &$__feeditem, ]); $feed[] = $__feeditem; } return $feed; } public function getData(int $widgetinstanceid) : array { $defaultContent = [ 'items' => [ ], 'url' => '', 'from_cache' => false, ]; $widgetApi = vB_Api::instanceInternal('widget'); try { $widgetConfig = $widgetApi->fetchConfig($widgetinstanceid); } catch (vB_Exception_Api $e) { return $defaultContent; } if ($widgetApi->doSkipModule($widgetinstanceid, $widgetConfig)) { return $defaultContent; } $url = $widgetConfig['url']; $maxEntries = $widgetConfig['maxEntries']; $updatePeriod__Seconds = $widgetConfig['updatePeriod']; $updatePeriod__Minutes = intval($updatePeriod__Seconds / 60); $cacheKey = 'vB_WidgetInstanceData_' . $widgetinstanceid . '_' . md5($url); if ($updatePeriod__Minutes > 0) { $cache = vB_Cache::instance(vB_Cache::CACHE_LARGE); $content = $cache->read($cacheKey); if ($content !== false AND $url === $content['url']) { $content['from_cache'] = true; return $content; } } $xml = $this->fetchRemoteXml($url); if ($xml === false) { return $defaultContent; } $entries = $this->parseRssOrAtomFeed($xml, $url); usort($entries, fn($a, $b) => ($b['timestamp'] - $a['timestamp'])); $content = []; $content['items'] = ($maxEntries > 0 ? array_slice($entries, 0, $maxEntries) : $entries); $content['url'] = $url; $content['from_cache'] = false; $cacheClearEvents = [ 'clearWidgetData_' . $widgetinstanceid ]; if ($updatePeriod__Minutes > 0) { $cache->write($cacheKey, $content, $updatePeriod__Minutes, $cacheClearEvents); } return $content; } private function stringifyAndConvert(vB_Utility_String $strUtil, SimpleXMlElement|string $value) : string { $value = (string) $value; try { $converted = $strUtil->toDefault($value, 'utf-8'); return $converted; } catch (Exception $e) { return $value; } } private function fetchRemoteXml(string $url) : SimpleXMLElement|false { $vurl = vB::getUrlLoader(); $vurl->setOption(vB_Utility_Url::FOLLOWLOCATION, 1); $vurl->setOption(vB_Utility_Url::TIMEOUT, 5); $vurl->setOption(vB_Utility_Url::ENCODING, 'gzip'); $vurl->setOption(vB_Utility_Url::HEADER, 1); vB::getHooks()->invoke('hookRssFetchXmlPreGet', [ 'url' => $url, 'vurl' => &$vurl, ]); $result = $vurl->get($url); if (empty($result['body'])) { return false; } if (!preg_match('#(<?xml.*encoding=[\'"])(.*?)([\'"].*?>)#m', $result['body'])) { $charset = $this->getCharsetFromHeaders($result['headers']); if ($charset) { $string = vB::getString(); $result['body'] = $string->toCharset($result['body'], $charset, 'utf-8'); } } $old = libxml_use_internal_errors(true); $xml = simplexml_load_string($result['body']); libxml_use_internal_errors($old); return $xml; } private function getCharsetFromHeaders($headers) { $contenttype = $headers['content-type'] ?? ''; if ($contenttype AND preg_match('#charset=(?<charset>[^()<>@,;:\\/\[\]\?\.=\s]+)#i', $contenttype, $matches)) { $charset = trim($matches['charset'], '"'); return $charset; } return ''; } } 