options), ), new SettingDefinition( key: 'feedUrl', type: 'url', default: (string)($this->options['feedUrl'] ?? ''), label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.fieldUrl.label', description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.fieldUrl.description', readonly: array_key_exists('feedUrl', $this->options), options: [ 'pattern' => 'https?://.+', ], ), new SettingDefinition( key: 'limit', type: 'int', default: (int)($this->options['limit'] ?? 5), label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.limit.label', description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.limit.description', readonly: array_key_exists('limit', $this->options), ), new SettingDefinition( key: 'lifeTime', type: 'int', default: (int)($this->options['lifeTime'] ?? 43200), label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.lifeTime.label', description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.lifeTime.description', readonly: true, ), ]; } public function renderWidget(WidgetContext $context): WidgetResult { $view = $this->backendViewFactory->create($context->request); $feedUrl = $context->settings->get('feedUrl'); $items = []; if ($feedUrl) { try { $items = $this->getFeedItems($context->settings); } catch (InvalidRssFeedException) { $view->assign('invalidFeed', true); } } $view->assignMultiple([ 'feedUrl' => $feedUrl, 'items' => $items, 'settings' => $context->settings, 'options' => $this->options, 'button' => $this->buttonProvider, 'configuration' => $this->configuration, ]); return new WidgetResult( label: $context->settings->get('label') !== '' ? $context->settings->get('label') : null, content: $view->render('Widget/RssWidget'), refreshable: true, ); } protected function getFeedItems(SettingsInterface $settings): array { $cacheHash = md5($settings->get('feedUrl') . '-' . $settings->get('limit')); if ($items = $this->cache->get($cacheHash)) { return $items; } $feedContent = GeneralUtility::getUrl($settings->get('feedUrl')); if ($feedContent === false) { throw new InvalidRssFeedException('RSS URL could not be fetched', 1573385431); } try { $feedXml = simplexml_load_string($feedContent); } catch (\Exception $e) { throw new InvalidRssFeedException('Received RSS feed could not be parsed.', 1573385432, $e); } $items = match ($this->determineFeedType($feedXml)) { 'atom' => $this->parseAtomFeed($feedXml), 'rss' => $this->parseRssFeed($feedXml), default => [] }; usort($items, static function ($item1, $item2) { return new \DateTime($item2['pubDate']) <=> new \DateTime($item1['pubDate']); }); $items = array_slice($items, 0, (int)$settings->get('limit')); $this->cache->set($cacheHash, $items, ['dashboard_rss'], (int)$settings->get('lifeTime')); return $items; } protected function determineFeedType(\SimpleXMLElement $feedXml): string { return $feedXml->getName() === 'feed' ? 'atom' : 'rss'; } protected function parseRssFeed(\SimpleXMLElement $rssFeed): array { $items = []; foreach ($rssFeed->channel->item as $item) { $items[] = [ 'title' => trim((string)$item->title), 'link' => trim((string)$item->link), 'pubDate' => trim((string)$item->pubDate), 'description' => trim(strip_tags((string)$item->description)), ]; } return $items; } protected function parseAtomFeed(\SimpleXMLElement $atomFeed): array { $items = []; foreach ($atomFeed->entry as $entry) { $items[] = [ 'title' => trim((string)$entry->title), 'link' => trim((string)($entry->link['href'] ?? '')), 'pubDate' => trim((string)($entry->published ?? $entry->updated ?? '')), 'description' => trim(strip_tags((string)($entry->summary ?? $entry->content ?? ''))), 'author' => [ 'name' => trim((string)($entry->author->name ?? '')), 'email' => trim((string)($entry->author->email ?? '')), 'url' => trim((string)($entry->author->url ?? '')), ], ]; } return $items; } }