Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion application/controllers/FeedController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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__');
});
Expand Down
64 changes: 27 additions & 37 deletions library/Feeds/FeedReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
}

/**
Expand Down
5 changes: 2 additions & 3 deletions library/Feeds/Parser/AtomParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
}
Expand Down
14 changes: 14 additions & 0 deletions library/Feeds/Parser/InvalidFeedDataException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Icinga\Module\Feeds\Parser;

use Icinga\Exception\IcingaException;

/**
* InvalidFeedDataException is a custom Exception we use in the feed auto detection
* to differentiate between errors detecting a feed type and a parsing error.
* Use InvalidFeedDataException to communicate parsing issues to the user.
*/
class InvalidFeedDataException extends IcingaException
{
}
14 changes: 14 additions & 0 deletions library/Feeds/Parser/InvalidFeedTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Icinga\Module\Feeds\Parser;

use Icinga\Exception\IcingaException;

/**
* InvalidFeedTypeException is a custom Exception we use in the feed auto detection
* to differentiate between errors detecting a feed type and a parsing error.
* Use InvalidFeedTypeException to tell the auto detection to try the next parser.
*/
class InvalidFeedTypeException extends IcingaException
{
}
23 changes: 17 additions & 6 deletions library/Feeds/Parser/JsonfeedParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Icinga\Module\Feeds\Parser\Result\FeedItem;

use Exception;
use JsonException;
use DateTime;
use DateTimeInterface;

Expand All @@ -14,17 +15,28 @@
*/
class JsonfeedParser
{
protected const JSONFEED_V10 = 'https://jsonfeed.org/version/1';
protected const JSONFEED_V11 = 'https://jsonfeed.org/version/1.1';

/**
* parse tries to parse the given string into a Feed object
*/
public static function parse(string $raw): Feed
{
$json = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
try {
$json = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
throw new InvalidFeedTypeException('Invalid JSONfeed');
}

if ($json === null) {
throw new Exception('Invalid JSONfeed');
throw new InvalidFeedTypeException('Invalid JSONfeed');
}

// TODO: validate version field
$version = $json['version'] ?? '';
if (!str_contains($version, self::JSONFEED_V10)) {
throw new InvalidFeedDataException('Unsupported JSONfeed version');
}

return static::parseFeed($json);
}
Expand All @@ -41,7 +53,7 @@ protected static function parseFeed(array $json): Feed
$items = $json['items'] ?? null;

if ($items === null) {
throw new Exception('JSONfeed contains no items');
throw new InvalidFeedDataException('JSONfeed contains no items');
}

foreach ($items as $jsonItem) {
Expand All @@ -63,8 +75,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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion library/Feeds/Parser/RSS1Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static function parse(string $raw): Feed
$xmlElement->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();
Expand Down
2 changes: 1 addition & 1 deletion library/Feeds/Parser/RSSParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
39 changes: 36 additions & 3 deletions test/php/library/Feeds/Parser/JsonfeedParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
}
}
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
61 changes: 61 additions & 0 deletions test/php/library/Feeds/Parser/testdata/jsonfeed1-1.json
Original file line number Diff line number Diff line change
@@ -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": "<p>Optional content for the feed reader. You may also use content_text or both at the same time.</p>",
"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"
}
]
}
7 changes: 7 additions & 0 deletions test/php/library/Feeds/Parser/testdata/jsonfeed2-0.json
Original file line number Diff line number Diff line change
@@ -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)"
}