From 45215685afd7e4d6483bb3d9fe71fbf3d4196a94 Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Wed, 3 Jun 2026 11:46:26 +0200 Subject: [PATCH] Add support for RSS1.0 feeds We probably could have done this in the existing RSSParser file, avoiding duplication, however, this way we keep the indiviual parsers simpler. --- application/forms/FeedForm.php | 3 +- library/Feeds/FeedReader.php | 11 +- library/Feeds/Parser/FeedType.php | 3 + library/Feeds/Parser/RSS1Parser.php | 152 ++++++++++++++++++++++++++++ library/Feeds/Parser/RSSParser.php | 4 +- 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 library/Feeds/Parser/RSS1Parser.php diff --git a/application/forms/FeedForm.php b/application/forms/FeedForm.php index b6de061..437923a 100644 --- a/application/forms/FeedForm.php +++ b/application/forms/FeedForm.php @@ -82,7 +82,8 @@ protected function assemble(): void ), 'multiOptions' => [ 'auto' => $this->translate('Determine automatically'), - 'rss' => $this->translate('RSS'), + 'rss' => $this->translate('RSS2.0'), + 'rss1.0' => $this->translate('RSS1.0'), 'atom' => $this->translate('Atom'), 'jsonfeed' => $this->translate('JSONfeed'), ], diff --git a/library/Feeds/FeedReader.php b/library/Feeds/FeedReader.php index 766d64b..44ee81b 100644 --- a/library/Feeds/FeedReader.php +++ b/library/Feeds/FeedReader.php @@ -6,6 +6,7 @@ use Icinga\Module\Feeds\Parser\FeedType; use Icinga\Module\Feeds\Parser\JsonfeedParser; use Icinga\Module\Feeds\Parser\RSSParser; +use Icinga\Module\Feeds\Parser\RSS1Parser; use Icinga\Module\Feeds\Parser\Result\Feed; use Icinga\Application\Config; @@ -78,7 +79,13 @@ protected function parse(string $rawResponse): ?Feed try { return RSSParser::parse($rawResponse); } catch (Exception $ex) { - // Not an RSS feed + // Not an RSS 2.0 feed + } + + try { + return RSS1Parser::parse($rawResponse); + } catch (Exception $ex) { + // Not an RSS 1.0 feed } try { @@ -96,6 +103,8 @@ protected function parse(string $rawResponse): ?Feed throw new Exception('Unsupported feed type or invalid data in feed'); case FeedType::RSS: return RSSParser::parse($rawResponse); + case FeedType::RSS1: + return RSS1Parser::parse($rawResponse); case FeedType::Atom: return AtomParser::parse($rawResponse); case FeedType::Jsonfeed: diff --git a/library/Feeds/Parser/FeedType.php b/library/Feeds/Parser/FeedType.php index 858b7c5..48bd654 100644 --- a/library/Feeds/Parser/FeedType.php +++ b/library/Feeds/Parser/FeedType.php @@ -13,6 +13,7 @@ enum FeedType: int case RSS = 1; case Atom = 2; case Jsonfeed = 3; + case RSS1 = 4; /** * display returns the string representation of the type @@ -22,6 +23,7 @@ public function display(): string return match ($this) { self::Auto => 'auto', self::RSS => 'rss', + self::RSS1 => 'rss1.0', self::Atom => 'atom', self::Jsonfeed => 'jsonfeed', default => throw new Exception('Unreachable code') @@ -36,6 +38,7 @@ public static function fromDisplay(string $display): static return match ($display) { 'auto' => self::Auto, 'rss' => self::RSS, + 'rss1.0' => self::RSS1, 'atom' => self::Atom, 'jsonfeed' => self::Jsonfeed, default => throw new Exception('Invalid FeedType') diff --git a/library/Feeds/Parser/RSS1Parser.php b/library/Feeds/Parser/RSS1Parser.php new file mode 100644 index 0000000..8734363 --- /dev/null +++ b/library/Feeds/Parser/RSS1Parser.php @@ -0,0 +1,152 @@ +registerXPathNamespace('rss', 'http://purl.org/rss/1.0/'); + $xmlElement->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/'); + + if ($xmlElement->getName() !== 'RDF') { + throw new Exception('Invalid RSS1.0-Feed'); + } + + $xmlElement->rewind(); + + return static::parseFeed($xmlElement); + } + + protected static function parseFeed(SimpleXMLElement $xml): Feed + { + $feed = new Feed(); + + foreach ($xml->getChildren() as $elementName => $xmlItemElement) { + switch ($elementName) { + case 'title': + $feed->title = $xmlItemElement->__toString(); + break; + case 'link': + $feed->link = $xmlItemElement->__toString(); + break; + case 'description': + $feed->description = $xmlItemElement->__toString(); + break; + case 'image': + foreach ($xmlItemElement as $imgTagName => $imgElement) { + if ($imgTagName === 'url') { + $feed->image = $imgElement->__toString(); + break; + } + } + break; + } + } + + $items = $xml->xpath('//rss:item'); + + foreach ($items as $xmlItemElement) { + $item = static::parseItem($feed, $xmlItemElement); + $feed->addItem($item); + } + + return $feed; + } + + protected static function parseDateTime(string $dateStr): ?DateTime + { + $datetime = DateTime::createFromFormat( + DateTimeInterface::RSS, + $dateStr, + ); + + // should be ISO 8601: https://web.resource.org/rss/1.0/modules/dc/ + if ($datetime === false) { + $datetime = DateTime::createFromFormat( + DateTimeInterface::ISO8601, + $dateStr, + ); + } + + // We probably don't need this, but let's keep it just in case + if ($datetime === false) { + $datetime = DateTime::createFromFormat( + DateTimeInterface::RFC822, + $dateStr, + ); + } + + if ($datetime === false) { + $datetime = DateTime::createFromFormat( + DateTimeInterface::RFC7231, + $dateStr, + ); + } + + if ($datetime === false) { + try { + $datetime = new DateTime($dateStr); + } catch (Exception $ex) { + // NOTE: Nothing to do here, but be ultimately failed to parse + // the time + $datetime = false; + } + } + + if ($datetime === false) { + return null; + } + + return $datetime; + } + + protected static function parseItem(Feed $feed, SimpleXMLElement $xml): FeedItem + { + $item = new FeedItem(); + $item->feed = $feed; + + foreach ($xml->children() as $elementName => $xmlItemElement) { + switch ($elementName) { + case 'title': + $item->title = $xmlItemElement->__toString(); + break; + case 'link': + $item->link = $xmlItemElement->__toString(); + break; + case 'description': + $item->description = $xmlItemElement->__toString(); + break; + } + } + + foreach ($xml->children('dc', true) as $elementName => $xmlItemElement) { + switch ($elementName) { + case 'date': + $dateString = $xmlItemElement->__toString(); + $item->date = static::parseDateTime($dateString); + break; + case 'creator': + $item->creator = $xmlItemElement->__toString(); + break; + } + } + + return $item; + } +} diff --git a/library/Feeds/Parser/RSSParser.php b/library/Feeds/Parser/RSSParser.php index d49ce19..9fae579 100644 --- a/library/Feeds/Parser/RSSParser.php +++ b/library/Feeds/Parser/RSSParser.php @@ -11,7 +11,7 @@ use DateTimeInterface; /** - * RSSParser is used to parse RSS feeds + * RSSParser is used to parse RSS 2.0 feeds */ class RSSParser { @@ -24,7 +24,7 @@ public static function parse(string $raw): Feed $xmlElement = new SimpleXMLElement($raw); if ($xmlElement->getName() !== 'rss') { - throw new Exception('Invalid RSS-Feed'); + throw new Exception('Invalid RSS2.0-Feed'); } $xmlElement->rewind();