diff --git a/application/controllers/FeedController.php b/application/controllers/FeedController.php index d2ff2e0..6228944 100644 --- a/application/controllers/FeedController.php +++ b/application/controllers/FeedController.php @@ -193,7 +193,6 @@ public function editAction(): void ]); $form->on(Form::ON_SUCCESS, function () { - // TODO: Should have a different message for deletion Notification::success($this->translate('Updated feeds')); $this->redirectNow('__CLOSE__'); }); diff --git a/library/Feeds/FeedReader.php b/library/Feeds/FeedReader.php index 42efd58..b9d1e49 100644 --- a/library/Feeds/FeedReader.php +++ b/library/Feeds/FeedReader.php @@ -8,6 +8,8 @@ use Icinga\Module\Feeds\Parser\RSSParser; use Icinga\Module\Feeds\Parser\RSS1Parser; use Icinga\Module\Feeds\Parser\Result\Feed; +use Icinga\Module\Feeds\Parser\InvalidFeedDataException; +use Icinga\Module\Feeds\Parser\InvalidFeedTypeException; use Icinga\Application\Config; use Icinga\Application\Icinga; @@ -73,44 +75,32 @@ protected function parse(string $rawResponse): ?Feed { Benchmark::measure('Started parsing feed'); - switch ($this->type) { - case FeedType::Auto: - try { - return RSSParser::parse($rawResponse); - } catch (Exception $ex) { - // Not an RSS 2.0 feed - } - - try { - return RSS1Parser::parse($rawResponse); - } catch (Exception $ex) { - // Not an RSS 1.0 feed - } - - try { - return AtomParser::parse($rawResponse); - } catch (Exception $ex) { - // Not an Atom feed - } - - try { - return JsonfeedParser::parse($rawResponse); - } catch (Exception $ex) { - // Not an JSONFeed 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: - return JsonfeedParser::parse($rawResponse); - default: - throw new Exception('Unsupported feed type'); + return match ($this->type) { + FeedType::Auto => $this->parseAuto($rawResponse), + FeedType::RSS => RSSParser::parse($rawResponse), + FeedType::RSS1 => RSS1Parser::parse($rawResponse), + FeedType::Atom => AtomParser::parse($rawResponse), + FeedType::Jsonfeed => JsonfeedParser::parse($rawResponse), + default => throw new InvalidFeedTypeException('Unsupported feed type'), + }; + } + + + protected function parseAuto(string $rawResponse): ?Feed + { + $parsers = [RSSParser::class, RSS1Parser::class, AtomParser::class, JsonfeedParser::class]; + + foreach ($parsers as $parser) { + try { + return $parser::parse($rawResponse); + } catch (InvalidFeedDataException $e) { + throw new Exception('Invalid data in feed: ' . $e->getMessage(), $e->getCode(), $e); + } catch (Exception) { + // Let's try the next format + } } + + throw new Exception('Unsupported feed type'); } /** diff --git a/library/Feeds/Parser/AtomParser.php b/library/Feeds/Parser/AtomParser.php index 4f89682..a058aff 100644 --- a/library/Feeds/Parser/AtomParser.php +++ b/library/Feeds/Parser/AtomParser.php @@ -24,7 +24,7 @@ public static function parse(string $raw): Feed $xmlElement = new SimpleXMLElement($raw); if ($xmlElement->getName() !== 'feed') { - throw new Exception('Invalid Atom-Feed'); + throw new InvalidFeedTypeException('Invalid Atom-Feed'); } $xmlElement->rewind(); @@ -174,8 +174,7 @@ protected static function parseDateTime(string $dateStr): ?DateTime try { $datetime = new DateTime($dateStr); } catch (Exception $ex) { - // NOTE: Nothing to do here, but be ultimately failed to parse - // the time + // NOTE: Nothing to do here, but be ultimately failed to parse the time $datetime = false; } } diff --git a/library/Feeds/Parser/InvalidFeedDataException.php b/library/Feeds/Parser/InvalidFeedDataException.php new file mode 100644 index 0000000..6b3210c --- /dev/null +++ b/library/Feeds/Parser/InvalidFeedDataException.php @@ -0,0 +1,14 @@ +registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/'); if ($xmlElement->getName() !== 'RDF') { - throw new Exception('Invalid RSS1.0-Feed'); + throw new InvalidFeedTypeException('Invalid RSS1.0-Feed'); } $xmlElement->rewind(); diff --git a/library/Feeds/Parser/RSSParser.php b/library/Feeds/Parser/RSSParser.php index ca18b1b..7b891e9 100644 --- a/library/Feeds/Parser/RSSParser.php +++ b/library/Feeds/Parser/RSSParser.php @@ -24,7 +24,7 @@ public static function parse(string $raw): Feed $xmlElement = new SimpleXMLElement($raw); if ($xmlElement->getName() !== 'rss') { - throw new Exception('Invalid RSS2.0-Feed'); + throw new InvalidFeedTypeException('Invalid RSS2.0-Feed'); } $xmlElement->rewind(); diff --git a/test/php/library/Feeds/Parser/JsonfeedParserTest.php b/test/php/library/Feeds/Parser/JsonfeedParserTest.php index 4480218..0d2123d 100644 --- a/test/php/library/Feeds/Parser/JsonfeedParserTest.php +++ b/test/php/library/Feeds/Parser/JsonfeedParserTest.php @@ -3,20 +3,53 @@ namespace Tests\Icinga\Module\Feeds\Parser; use Icinga\Module\Feeds\Parser\JsonfeedParser; +use Icinga\Module\Feeds\Parser\InvalidFeedDataException; +use Icinga\Module\Feeds\Parser\InvalidFeedTypeException; use Icinga\Module\Feeds\Parser\Result\Feed; use PHPUnit\Framework\TestCase; final class JsonfeedParserTest extends TestCase { - public function testParse() + // https://jsonfeed.org/version/1.0 + public function testParseJsonFeed10() { - $raw = file_get_contents(__DIR__ .'/testdata/jsonfeed.json'); + $raw = file_get_contents(__DIR__ .'/testdata/jsonfeed1-0.json'); $actual = JsonfeedParser::parse($raw); $this->assertTrue($actual instanceof Feed); - $this->assertSame($actual->title, 'JSONFeed Sample Feed'); + $this->assertSame($actual->title, 'JSONFeed 1.0 Sample Feed'); $this->assertSame(count($actual->getItems()), 1); } + + // https://jsonfeed.org/version/1.1 + public function testParseJsonFeed11() + { + $raw = file_get_contents(__DIR__ .'/testdata/jsonfeed1-1.json'); + + $actual = JsonfeedParser::parse($raw); + + $this->assertTrue($actual instanceof Feed); + $this->assertSame($actual->title, 'JSONFeed 1.1 Sample Feed'); + $this->assertSame(count($actual->getItems()), 2); + } + + // Does not exist (yet) + public function testParseJsonFeed20() + { + $raw = file_get_contents(__DIR__ .'/testdata/jsonfeed2-0.json'); + + $this->expectException(InvalidFeedDataException::class); + + $actual = JsonfeedParser::parse($raw); + } + + // With invalid input + public function testParseJsonFeed_NoData() + { + $this->expectException(InvalidFeedTypeException::class); + + $actual = JsonfeedParser::parse(''); + } } diff --git a/test/php/library/Feeds/Parser/testdata/jsonfeed.json b/test/php/library/Feeds/Parser/testdata/jsonfeed1-0.json similarity index 98% rename from test/php/library/Feeds/Parser/testdata/jsonfeed.json rename to test/php/library/Feeds/Parser/testdata/jsonfeed1-0.json index e6d5e26..e62f6e9 100644 --- a/test/php/library/Feeds/Parser/testdata/jsonfeed.json +++ b/test/php/library/Feeds/Parser/testdata/jsonfeed1-0.json @@ -1,6 +1,6 @@ { "version": "https://jsonfeed.org/version/1", - "title": "JSONFeed Sample Feed", + "title": "JSONFeed 1.0 Sample Feed", "home_page_url": "https://example.org/homepage", "feed_url": "https://example.org/feed.json", "description": "This is my feed description", diff --git a/test/php/library/Feeds/Parser/testdata/jsonfeed1-1.json b/test/php/library/Feeds/Parser/testdata/jsonfeed1-1.json new file mode 100644 index 0000000..7316742 --- /dev/null +++ b/test/php/library/Feeds/Parser/testdata/jsonfeed1-1.json @@ -0,0 +1,61 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "JSONFeed 1.1 Sample Feed", + "home_page_url": "https://example.org/", + "feed_url": "https://example.org/feed.json", + "description": "Optional to provide more detail beyond the title.", + "user_comment": "Optional and should be ignored by feed readers.", + "next_url": "https://example.org/pagination?feed=feed.json&p=17", + "icon": "https://example.org/favicon-timeline-512x512.png", + "favicon": "https://example.org/favicon-sourcelist-64x64.png", + "authors": [ + { + "name": "Optional Author", + "url": "https://example.org/authors/optional-author", + "avatar": "https://example.org/authors/optional-author/avatar-512x512.png" + } + ], + "language": "en-US", + "items": [ + { + "id": "2", + "content_text": "This is a second item.", + "url": "https://example.org/second-item", + "language": "es-mx", + "attachments": [ + { + "url": "https://example.org/second-item/audio.ogg", + "mime_type": "audio/ogg", + "title": "Optional Title", + "size_in_bytes": 31415927, + "duration_in_seconds": 1800 + } + ] + }, + { + "id": "required-unique-string-that-does-not-change: number, guid, url, etc.", + "url": "https://example.org/initial-post", + "external_url": "https://en.wikipedia.org/w/index.php?title=JSON_Feed", + "title": "Optional Title", + "content_html": "

Optional content for the feed reader. You may also use content_text or both at the same time.

", + "content_text": "Optional text for simple feeds.", + "summary": "Optional summary of the item.", + "image": "https://example.org/initial-post/main-img.png", + "banner_image": "https://example.org/initial-post/details-banner.png", + "date_published": "2021-10-25T19:30:00-01:00", + "date_modified": "2021-10-26T19:45:00-01:00", + "authors": [ + { + "name": "Optional Author", + "url": "https://example.org/authors/optional-author", + "avatar": "https://example.org/authors/optional-author/avatar-512x512.png" + } + ], + "tags": [ + "Optional Tag", + "Example" + ], + "language": "en-US" + } + ] +} diff --git a/test/php/library/Feeds/Parser/testdata/jsonfeed2-0.json b/test/php/library/Feeds/Parser/testdata/jsonfeed2-0.json new file mode 100644 index 0000000..80cc025 --- /dev/null +++ b/test/php/library/Feeds/Parser/testdata/jsonfeed2-0.json @@ -0,0 +1,7 @@ +{ + "version": "https://jsonfeed.org/version/2", + "title": "JSONFeed 2.0", + "home_page_url": "https://example.org/homepage", + "feed_url": "https://example.org/feed.json", + "description": "JSONFeed 2.0 does not exist (yet)" +}