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
|
// Must be one of news, meetings, user_shifts, angeltypes, questions
|
||||||
'home_site' => env('HOME_SITE', 'news'),
|
'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),
|
'display_news' => env('DISPLAY_NEWS', 10),
|
||||||
|
|
||||||
// Users are able to sign up
|
// Users are able to sign up
|
||||||
|
|
|
@ -96,6 +96,10 @@ $route->addGroup(
|
||||||
// API
|
// API
|
||||||
$route->get('/api[/{resource:.+}]', 'ApiController@index');
|
$route->get('/api[/{resource:.+}]', 'ApiController@index');
|
||||||
|
|
||||||
|
// Feeds
|
||||||
|
$route->get('/atom', 'FeedController@atom');
|
||||||
|
$route->get('/rss', 'FeedController@rss');
|
||||||
|
|
||||||
// Design
|
// Design
|
||||||
$route->get('/design', 'DesignController@index');
|
$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'), [
|
return page_with_title(__('Reset API key'), [
|
||||||
error(
|
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
|
true
|
||||||
),
|
),
|
||||||
button(page_link_to('user_myshifts', ['reset' => 'ack']), __('Continue'), 'btn-danger')
|
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
|
#: includes/pages/user_myshifts.php:44
|
||||||
msgid ""
|
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 "
|
"feed changes! You have to update it in every application using one of these "
|
||||||
"exports."
|
"exports."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wenn du den API-Key zurücksetzt, ändert sich die URL zu deinem iCal-, JSON-"
|
"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
|
#: includes/pages/user_myshifts.php:47
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
|
|
|
@ -1500,13 +1500,13 @@ msgstr "Resetar a chave API"
|
||||||
|
|
||||||
#: includes/pages/user_myshifts.php:27
|
#: includes/pages/user_myshifts.php:27
|
||||||
msgid ""
|
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 "
|
"feed changes! You have to update it in every application using one of these "
|
||||||
"exports."
|
"exports."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Se você reconfigurar a chave, as urls para seu iCal, a exportação em formato "
|
"Se você reconfigurar a chave, as urls para seu iCal, a exportação em formato "
|
||||||
"JSON \n"
|
"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"
|
"aplicações que estiverem \n"
|
||||||
"usando uma delas."
|
"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) -%}
|
{% set parameters = {'meetings': 1}|merge(parameters) -%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<link href="{{ url('atom', parameters) }}" type="application/atom+xml" rel="alternate" title="Atom Feed">
|
<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 %}
|
{% endblock %}
|
||||||
</head>
|
</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 = [
|
protected array $free_pages = [
|
||||||
'admin_event_config',
|
'admin_event_config',
|
||||||
'angeltypes',
|
'angeltypes',
|
||||||
'atom',
|
|
||||||
'ical',
|
'ical',
|
||||||
'public_dashboard',
|
'public_dashboard',
|
||||||
'rooms',
|
'rooms',
|
||||||
|
@ -83,10 +82,6 @@ class LegacyMiddleware implements MiddlewareInterface
|
||||||
require_once realpath(__DIR__ . '/../../includes/pages/user_ical.php');
|
require_once realpath(__DIR__ . '/../../includes/pages/user_ical.php');
|
||||||
user_ical();
|
user_ical();
|
||||||
break;
|
break;
|
||||||
case 'atom':
|
|
||||||
require_once realpath(__DIR__ . '/../../includes/pages/user_atom.php');
|
|
||||||
user_atom();
|
|
||||||
break;
|
|
||||||
case 'shifts_json_export':
|
case 'shifts_json_export':
|
||||||
require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php');
|
require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php');
|
||||||
shifts_json_export_controller();
|
shifts_json_export_controller();
|
||||||
|
|
|
@ -15,6 +15,7 @@ class SessionHandlerServiceProvider extends ServiceProvider
|
||||||
return [
|
return [
|
||||||
'/api',
|
'/api',
|
||||||
'/atom',
|
'/atom',
|
||||||
|
'/rss',
|
||||||
'/health',
|
'/health',
|
||||||
'/ical',
|
'/ical',
|
||||||
'/metrics',
|
'/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