Reimplemented news Atom feed, added RSS feed
This commit is contained in:
parent
8223193330
commit
3d0d5067fd
|
@ -216,7 +216,7 @@ return [
|
|||
// Must be one of news, meetings, user_shifts, angeltypes, questions
|
||||
'home_site' => env('HOME_SITE', 'news'),
|
||||
|
||||
// Number of News shown on one site
|
||||
// Number of News shown on one site and for feed readers
|
||||
'display_news' => env('DISPLAY_NEWS', 10),
|
||||
|
||||
// Users are able to sign up
|
||||
|
|
|
@ -96,6 +96,10 @@ $route->addGroup(
|
|||
// API
|
||||
$route->get('/api[/{resource:.+}]', 'ApiController@index');
|
||||
|
||||
// Feeds
|
||||
$route->get('/atom', 'FeedController@atom');
|
||||
$route->get('/rss', 'FeedController@rss');
|
||||
|
||||
// Design
|
||||
$route->get('/design', 'DesignController@index');
|
||||
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Engelsystem\Http\Exceptions\HttpForbidden;
|
||||
use Engelsystem\Models\News;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Collection as SupportCollection;
|
||||
|
||||
/**
|
||||
* Publically available page to feed the news to feed readers
|
||||
*/
|
||||
function user_atom()
|
||||
{
|
||||
$request = request();
|
||||
$user = auth()->userFromApi();
|
||||
|
||||
if (!$user) {
|
||||
throw new HttpForbidden('Missing or invalid ?key=', ['content-type' => 'text/text']);
|
||||
}
|
||||
|
||||
if (!auth()->can('atom')) {
|
||||
throw new HttpForbidden('Not allowed', ['content-type' => 'text/text']);
|
||||
}
|
||||
|
||||
$news = $request->has('meetings') ? News::whereIsMeeting((bool) $request->get('meetings', false)) : News::query();
|
||||
$news
|
||||
->limit((int) config('display_news'))
|
||||
->orderByDesc('updated_at');
|
||||
$output = make_atom_entries_from_news($news->get());
|
||||
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
header('Content-Length: ' . strlen($output));
|
||||
raw_output($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param News[]|Collection|SupportCollection $news_entries
|
||||
* @return string
|
||||
*/
|
||||
function make_atom_entries_from_news($news_entries)
|
||||
{
|
||||
$request = app('request');
|
||||
$updatedAt = isset($news_entries[0]) ? $news_entries[0]->updated_at->format('Y-m-d\TH:i:sP') : '0000:00:00T00:00:00+00:00';
|
||||
|
||||
$html = '<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>' . config('app_name') . '</title>
|
||||
<id>' . $request->getHttpHost()
|
||||
. htmlspecialchars(preg_replace(
|
||||
'#[&?]key=[a-f\d]+#',
|
||||
'',
|
||||
$request->getRequestUri()
|
||||
))
|
||||
. '</id>
|
||||
<updated>' . $updatedAt . '</updated>' . "\n";
|
||||
foreach ($news_entries as $news_entry) {
|
||||
$html .= make_atom_entry_from_news($news_entry);
|
||||
}
|
||||
$html .= '</feed>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param News $news
|
||||
* @return string
|
||||
*/
|
||||
function make_atom_entry_from_news(News $news)
|
||||
{
|
||||
return '
|
||||
<entry>
|
||||
<title>' . htmlspecialchars($news->title) . '</title>
|
||||
<link href="' . page_link_to('news/' . $news->id) . '"/>
|
||||
<id>' . preg_replace(
|
||||
'#^https?://#',
|
||||
'',
|
||||
page_link_to('news/' . $news->id)
|
||||
) . '</id>
|
||||
<updated>' . $news->updated_at->format('Y-m-d\TH:i:sP') . '</updated>
|
||||
<summary type="html">' . htmlspecialchars($news->text) . '</summary>
|
||||
</entry>' . "\n";
|
||||
}
|
|
@ -41,7 +41,7 @@ function user_myshifts()
|
|||
}
|
||||
return page_with_title(__('Reset API key'), [
|
||||
error(
|
||||
__('If you reset the key, the url to your iCal- and JSON-export and your atom feed changes! You have to update it in every application using one of these exports.'),
|
||||
__('If you reset the key, the url to your iCal- and JSON-export and your atom/rss feed changes! You have to update it in every application using one of these exports.'),
|
||||
true
|
||||
),
|
||||
button(page_link_to('user_myshifts', ['reset' => 'ack']), __('Continue'), 'btn-danger')
|
||||
|
|
|
@ -1733,12 +1733,12 @@ msgstr "API-Key zurücksetzen"
|
|||
|
||||
#: includes/pages/user_myshifts.php:44
|
||||
msgid ""
|
||||
"If you reset the key, the url to your iCal- and JSON-export and your atom "
|
||||
"If you reset the key, the url to your iCal- and JSON-export and your atom/rss "
|
||||
"feed changes! You have to update it in every application using one of these "
|
||||
"exports."
|
||||
msgstr ""
|
||||
"Wenn du den API-Key zurücksetzt, ändert sich die URL zu deinem iCal-, JSON-"
|
||||
"Export und Atom Feed! Du musst diesen überall ändern, wo er in Benutzung ist."
|
||||
"Export und Atom/RSS Feed! Du musst diesen überall ändern, wo er in Benutzung ist."
|
||||
|
||||
#: includes/pages/user_myshifts.php:47
|
||||
msgid "Continue"
|
||||
|
|
|
@ -1500,13 +1500,13 @@ msgstr "Resetar a chave API"
|
|||
|
||||
#: includes/pages/user_myshifts.php:27
|
||||
msgid ""
|
||||
"If you reset the key, the url to your iCal- and JSON-export and your atom "
|
||||
"If you reset the key, the url to your iCal- and JSON-export and your atom/rss "
|
||||
"feed changes! You have to update it in every application using one of these "
|
||||
"exports."
|
||||
msgstr ""
|
||||
"Se você reconfigurar a chave, as urls para seu iCal, a exportação em formato "
|
||||
"JSON \n"
|
||||
"e seu feed atom será alterada! Você precisará atualizá-las em todas as "
|
||||
"e seu feed atom/rss será alterada! Você precisará atualizá-las em todas as "
|
||||
"aplicações que estiverem \n"
|
||||
"usando uma delas."
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{% set dateFormat = 'Y-m-d\\TH:i:sP' %}
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
<id>{{ url('/') }}</id>
|
||||
<title>{{ config('app_name') }}</title>
|
||||
<updated>{% if not news.isEmpty() %}{{ news[0].updated_at.format(dateFormat) }}{% else %}0000-00-00T00:00:00+00:00{% endif %}</updated>
|
||||
<link rel="self" type="application/atom+xml" href="{{ url('/atom', {'key': request.get('key') }) }}"/>
|
||||
|
||||
{% for news in news %}
|
||||
|
||||
<entry>
|
||||
<title>{% if news.is_meeting %}{{ __('news.is_meeting') }} {% endif %}{{ news.title }}</title>
|
||||
<id>urn:uuid:{{ uuidBy(news.id, '113115') }}</id>
|
||||
<link href="{{ url('/news/' ~ news.id) }}"/>
|
||||
<updated>{{ news.updated_at.format(dateFormat) }}</updated>
|
||||
<published>{{ news.created_at.format(dateFormat) }}</published>
|
||||
<author>
|
||||
<name>{{ news.user.name }}</name>
|
||||
</author>
|
||||
<content type="html">
|
||||
{{ news.text }}
|
||||
</content>
|
||||
</entry>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</feed>
|
|
@ -0,0 +1,28 @@
|
|||
{% set dateFormat = 'D, d M Y H:i:s O' %}
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
|
||||
<title>{{ config('app_name') }}</title>
|
||||
<link>{{ url('/') }}</link>
|
||||
<description></description>
|
||||
<atom:link href="{{ url('/atom', {'key': request.get('key') }) }}" rel="self" type="application/rss+xml"/>
|
||||
<lastBuildDate>{% if not news.isEmpty() %}{{ news[0].updated_at.format(dateFormat) }}{% else %}Fri, 31 Dec 00 00:00:00 +0000{% endif %}</lastBuildDate>
|
||||
|
||||
{% for news in news %}
|
||||
|
||||
<item>
|
||||
<title>{% if news.is_meeting %}{{ __('news.is_meeting') }} {% endif %}{{ news.title }}</title>
|
||||
<link>{{ url('/news/' ~ news.id) }}</link>
|
||||
<guid isPermaLink="false">{{ uuidBy(news.id, '113115') }}</guid>
|
||||
<pubDate>{{ news.created_at.format(dateFormat) }}</pubDate>
|
||||
<author>{{ news.user.name }}</author>
|
||||
<description>
|
||||
{{ news.text }}
|
||||
</description>
|
||||
</item>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</channel>
|
||||
</rss>
|
|
@ -18,7 +18,8 @@
|
|||
{% set parameters = {'meetings': 1}|merge(parameters) -%}
|
||||
{% endif %}
|
||||
<link href="{{ url('atom', parameters) }}" type="application/atom+xml" rel="alternate" title="Atom Feed">
|
||||
{% endif %}
|
||||
<link href="{{ url('rss', parameters) }}" type="application/rss+xml" rel="alternate" title="RSS Feed" />
|
||||
{%- endif %}
|
||||
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace Engelsystem\Controllers;
|
||||
|
||||
use Engelsystem\Http\Request;
|
||||
use Engelsystem\Http\Response;
|
||||
use Engelsystem\Models\News;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class FeedController extends BaseController
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
protected array $permissions = [
|
||||
'atom' => 'atom',
|
||||
'rss' => 'atom',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Request $request,
|
||||
protected Response $response,
|
||||
) {
|
||||
}
|
||||
|
||||
public function atom(): Response
|
||||
{
|
||||
$news = $this->getNews();
|
||||
|
||||
return $this->response
|
||||
->withHeader('content-type', 'application/atom+xml; charset=utf-8')
|
||||
->withView('api/atom', ['news' => $news]);
|
||||
}
|
||||
|
||||
public function rss(): Response
|
||||
{
|
||||
$news = $this->getNews();
|
||||
|
||||
return $this->response
|
||||
->withHeader('content-type', 'application/rss+xml; charset=utf-8')
|
||||
->withView('api/rss', ['news' => $news]);
|
||||
}
|
||||
|
||||
protected function getNews(): Collection
|
||||
{
|
||||
$news = $this->request->has('meetings')
|
||||
? News::whereIsMeeting((bool) $this->request->get('meetings', false))
|
||||
: News::query();
|
||||
$news
|
||||
->limit((int) config('display_news'))
|
||||
->orderByDesc('updated_at');
|
||||
|
||||
return $news->get();
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ class LegacyMiddleware implements MiddlewareInterface
|
|||
protected array $free_pages = [
|
||||
'admin_event_config',
|
||||
'angeltypes',
|
||||
'atom',
|
||||
'ical',
|
||||
'public_dashboard',
|
||||
'rooms',
|
||||
|
@ -83,10 +82,6 @@ class LegacyMiddleware implements MiddlewareInterface
|
|||
require_once realpath(__DIR__ . '/../../includes/pages/user_ical.php');
|
||||
user_ical();
|
||||
break;
|
||||
case 'atom':
|
||||
require_once realpath(__DIR__ . '/../../includes/pages/user_atom.php');
|
||||
user_atom();
|
||||
break;
|
||||
case 'shifts_json_export':
|
||||
require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php');
|
||||
shifts_json_export_controller();
|
||||
|
|
|
@ -15,6 +15,7 @@ class SessionHandlerServiceProvider extends ServiceProvider
|
|||
return [
|
||||
'/api',
|
||||
'/atom',
|
||||
'/rss',
|
||||
'/health',
|
||||
'/ical',
|
||||
'/metrics',
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace Engelsystem\Test\Unit\Controllers;
|
||||
|
||||
use Engelsystem\Controllers\FeedController;
|
||||
use Engelsystem\Helpers\Carbon;
|
||||
use Engelsystem\Models\News;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class FeedControllerTest extends ControllerTest
|
||||
{
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\FeedController::__construct
|
||||
* @covers \Engelsystem\Controllers\FeedController::atom
|
||||
*/
|
||||
public function testAtom(): void
|
||||
{
|
||||
$controller = new FeedController($this->request, $this->response);
|
||||
|
||||
$this->setExpects(
|
||||
$this->response,
|
||||
'withHeader',
|
||||
['content-type', 'application/atom+xml; charset=utf-8'],
|
||||
$this->response
|
||||
);
|
||||
$this->response->expects($this->once())
|
||||
->method('withView')
|
||||
->willReturnCallback(function ($view, $data) {
|
||||
$this->assertEquals('api/atom', $view);
|
||||
$this->assertArrayHasKey('news', $data);
|
||||
|
||||
return $this->response;
|
||||
});
|
||||
|
||||
$controller->atom();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\FeedController::rss
|
||||
*/
|
||||
public function testRss(): void
|
||||
{
|
||||
$controller = new FeedController($this->request, $this->response);
|
||||
|
||||
$this->setExpects(
|
||||
$this->response,
|
||||
'withHeader',
|
||||
['content-type', 'application/rss+xml; charset=utf-8'],
|
||||
$this->response
|
||||
);
|
||||
$this->response->expects($this->once())
|
||||
->method('withView')
|
||||
->willReturnCallback(function ($view, $data) {
|
||||
$this->assertEquals('api/rss', $view);
|
||||
$this->assertArrayHasKey('news', $data);
|
||||
|
||||
return $this->response;
|
||||
});
|
||||
$controller->rss();
|
||||
}
|
||||
|
||||
public function getNewsMeetingsDataProvider(): array
|
||||
{
|
||||
return [
|
||||
[true],
|
||||
[false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\FeedController::getNews
|
||||
* @dataProvider getNewsMeetingsDataProvider
|
||||
*/
|
||||
public function testGetNewsMeetings(bool $isMeeting): void
|
||||
{
|
||||
$controller = new FeedController($this->request, $this->response);
|
||||
|
||||
$this->request->attributes->set('meetings', $isMeeting);
|
||||
$this->setExpects($this->response, 'withHeader', null, $this->response);
|
||||
$this->response->expects($this->once())
|
||||
->method('withView')
|
||||
->willReturnCallback(function ($view, $data) use ($isMeeting) {
|
||||
/** @var Collection|News[] $newsList */
|
||||
$newsList = $data['news'];
|
||||
$this->assertCount($isMeeting ? 5 : 7, $newsList);
|
||||
|
||||
foreach ($newsList as $news) {
|
||||
$this->assertEquals($isMeeting, $news->is_meeting);
|
||||
}
|
||||
|
||||
return $this->response;
|
||||
});
|
||||
|
||||
$controller->rss();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\FeedController::getNews
|
||||
*/
|
||||
public function testGetNewsLimit(): void
|
||||
{
|
||||
News::query()->where('id', '<>', 1)->update(['updated_at' => Carbon::now()->subHour()]);
|
||||
$controller = new FeedController($this->request, $this->response);
|
||||
|
||||
$this->setExpects($this->response, 'withHeader', null, $this->response);
|
||||
$this->response->expects($this->once())
|
||||
->method('withView')
|
||||
->willReturnCallback(function ($view, $data) {
|
||||
$this->assertCount(10, $data['news']);
|
||||
|
||||
/** @var News $news1 */
|
||||
$news1 = $data['news'][0];
|
||||
/** @var News $news2 */
|
||||
$news2 = $data['news'][1];
|
||||
$this->assertTrue($news1->updated_at > $news2->updated_at, 'First news must be up to date');
|
||||
|
||||
return $this->response;
|
||||
});
|
||||
|
||||
$controller->rss();
|
||||
}
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->config->set('display_news', 10);
|
||||
|
||||
News::factory(7)->create(['is_meeting' => false]);
|
||||
News::factory(5)->create(['is_meeting' => true]);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue