diff --git a/config/routes.php b/config/routes.php index 9bf9090b..e4bd9888 100644 --- a/config/routes.php +++ b/config/routes.php @@ -100,6 +100,7 @@ $route->get('/api[/{resource:.+}]', 'ApiController@index'); $route->get('/atom', 'FeedController@atom'); $route->get('/ical', 'FeedController@ical'); $route->get('/rss', 'FeedController@rss'); +$route->get('/shifts-json-export', 'FeedController@shifts'); // Design $route->get('/design', 'DesignController@index'); diff --git a/includes/controller/shifts_controller.php b/includes/controller/shifts_controller.php index f776a581..054cc33d 100644 --- a/includes/controller/shifts_controller.php +++ b/includes/controller/shifts_controller.php @@ -1,7 +1,5 @@ userFromApi(); - - if (!$user) { - throw new HttpForbidden('{"error":"Missing or invalid ?key="}', ['content-type' => 'application/json']); - } - - if (!auth()->can('shifts_json_export')) { - throw new HttpForbidden('{"error":"Not allowed"}', ['content-type' => 'application/json']); - } - - $shifts = Shifts_by_user(auth()->user()->id); - $shifts->sortBy('start_date'); - $timeZone = CarbonTimeZone::create(config('timezone')); - - $shiftsData = []; - foreach ($shifts as $shift) { - // Data required for the Fahrplan app integration https://github.com/johnjohndoe/engelsystem - // See engelsystem-base/src/main/kotlin/info/metadude/kotlin/library/engelsystem/models/Shift.kt - $data = [ - // Name of the shift (type) - 'name' => $shift->shiftType->name, - // Shift / Talk title - 'title' => $shift->title, - // Shift description - 'description' => $shift->description, - - // Users comment - 'Comment' => $shift->user_comment, - - // Shift id - 'SID' => $shift->id, - // Shift type id - 'shifttype_id' => $shift->shift_type_id, - // Talk URL - 'URL' => $shift->url, - - // Room name - 'Name' => $shift->room->name, - // Location map url - 'map_url' => $shift->room->map_url, - - // Start timestamp - /** @deprecated start_date should be used */ - 'start' => $shift->start->timestamp, - // Start date - 'start_date' => $shift->start->toRfc3339String(), - // End timestamp - /** @deprecated end_date should be used */ - 'end' => $shift->end->timestamp, - // End date - 'end_date' => $shift->end->toRfc3339String(), - - // Timezone offset like "+01:00" - /** @deprecated should be retrieved from start_date or end_date */ - 'timezone' => $timeZone->toOffsetName(), - // The events timezone like "Europe/Berlin" - 'event_timezone' => $timeZone->getName(), - ]; - - $shiftsData[] = [ - // Model data - ...$shift->toArray(), - - // legacy fields (ignoring created / updated (at/by) data) - 'RID' => $shift->room_id, - - // Fahrplan app required data - ...$data - ]; - } - - header('Content-Type: application/json; charset=utf-8'); - raw_output(json_encode($shiftsData)); -} diff --git a/src/Controllers/FeedController.php b/src/Controllers/FeedController.php index 0bd55732..eeab0529 100644 --- a/src/Controllers/FeedController.php +++ b/src/Controllers/FeedController.php @@ -2,19 +2,22 @@ namespace Engelsystem\Controllers; +use Carbon\CarbonTimeZone; use Engelsystem\Helpers\Authenticator; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Models\News; +use Engelsystem\Models\Shifts\ShiftEntry; use Illuminate\Support\Collection; class FeedController extends BaseController { /** @var array */ protected array $permissions = [ - 'atom' => 'atom', - 'rss' => 'atom', - 'ical' => 'ical', + 'atom' => 'atom', + 'rss' => 'atom', + 'ical' => 'ical', + 'shifts' => 'shifts_json_export', ]; public function __construct( @@ -52,6 +55,75 @@ class FeedController extends BaseController ->withView('api/ical', ['shiftEntries' => $shifts]); } + public function shifts(): Response + { + /** @var Collection|ShiftEntry[] $shiftEntries */ + $shiftEntries = $this->getShifts(); + $timeZone = CarbonTimeZone::create(config('timezone')); + + $response = []; + foreach ($shiftEntries as $entry) { + $shift = $entry->shift; + // Data required for the Fahrplan app integration https://github.com/johnjohndoe/engelsystem + // See engelsystem-base/src/main/kotlin/info/metadude/kotlin/library/engelsystem/models/Shift.kt + // ! All attributes not defined in $data might change at any time ! + $data = [ + // Name of the shift (type) + 'name' => $shift->shiftType->name, + // Shift / Talk title + 'title' => $shift->title, + // Shift description + 'description' => $shift->description, + + // Users comment + 'Comment' => $entry->user_comment, + + // Shift id + 'SID' => $shift->id, + // Shift type id + 'shifttype_id' => $shift->shiftType->id, + // Talk URL + 'URL' => $shift->url, + + // Room id + 'RID' => $shift->room->id, + // Room name + 'Name' => $shift->room->name, + // Location map url + 'map_url' => $shift->room->map_url, + + // Start timestamp + /** @deprecated start_date should be used */ + 'start' => $shift->start->timestamp, + // Start date + 'start_date' => $shift->start->toRfc3339String(), + // End timestamp + /** @deprecated end_date should be used */ + 'end' => $shift->end->timestamp, + // End date + 'end_date' => $shift->end->toRfc3339String(), + + // Timezone offset like "+01:00" + /** @deprecated should be retrieved from start_date or end_date */ + 'timezone' => $timeZone->toOffsetName(), + // The events timezone like "Europe/Berlin" + 'event_timezone' => $timeZone->getName(), + ]; + + $response[] = [ + // Model data + ...$entry->toArray(), + + // Fahrplan app required data + ...$data + ]; + } + + return $this->response + ->withAddedHeader('content-type', 'application/json; charset=utf-8') + ->withContent(json_encode($response)); + } + protected function getNews(): Collection { $news = $this->request->has('meetings') diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 46793479..72a990c7 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -21,7 +21,6 @@ class LegacyMiddleware implements MiddlewareInterface 'rooms', 'shift_entries', 'shifts', - 'shifts_json_export', 'users', 'user_driver_licenses', 'admin_shifts_history', @@ -77,10 +76,6 @@ class LegacyMiddleware implements MiddlewareInterface protected function loadPage(string $page): array { switch ($page) { - case 'shifts_json_export': - require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php'); - shifts_json_export_controller(); - break; case 'public_dashboard': return public_dashboard_controller(); case 'angeltypes': diff --git a/src/Models/Shifts/ShiftEntry.php b/src/Models/Shifts/ShiftEntry.php index 5014bce1..632f5649 100644 --- a/src/Models/Shifts/ShiftEntry.php +++ b/src/Models/Shifts/ShiftEntry.php @@ -51,6 +51,16 @@ class ShiftEntry extends BaseModel 'freeloaded_comment' => '', ]; + /** @var array */ + protected $casts = [ // phpcs:ignore + 'freeloaded' => 'bool', + ]; + + /** @var array Attributes which should not be serialized */ + protected $hidden = [ // phpcs:ignore + 'freeloaded_comment', + ]; + public function shift(): BelongsTo { return $this->belongsTo(Shift::class); diff --git a/tests/Unit/Controllers/FeedControllerTest.php b/tests/Unit/Controllers/FeedControllerTest.php index c8266924..68b51f45 100644 --- a/tests/Unit/Controllers/FeedControllerTest.php +++ b/tests/Unit/Controllers/FeedControllerTest.php @@ -110,6 +110,59 @@ class FeedControllerTest extends ControllerTest $controller->ical(); } + /** + * @covers \Engelsystem\Controllers\FeedController::shifts + * @covers \Engelsystem\Controllers\FeedController::getShifts + */ + public function testShifts(): void + { + $this->request = $this->request->withQueryParams(['key' => 'fo0']); + $this->auth = new Authenticator( + $this->request, + new Session(new MockArraySessionStorage()), + new User(), + ); + $controller = new FeedController($this->auth, $this->request, $this->response); + + /** @var User $user */ + $user = User::factory()->create(['api_key' => 'fo0']); + ShiftEntry::factory(3)->create(['user_id' => $user->id]); + + $this->setExpects( + $this->response, + 'withAddedHeader', + ['content-type', 'application/json; charset=utf-8'], + $this->response + ); + + $this->response->expects($this->once()) + ->method('withContent') + ->willReturnCallback(function ($jsonData) { + $data = json_decode($jsonData, true); + $this->assertIsArray($data); + + $this->assertCount(3, $data); + $this->assertTrue($data[0]['start'] < $data[1]['start']); + + // Ensure dates exist used by Fahrplan app + foreach ( + [ + 'name', 'title', 'description', + 'Comment', + 'SID', 'shifttype_id', 'URL', + 'RID', 'Name', 'map_url', + 'start', 'start_date', 'end', 'end_date', + 'timezone', 'event_timezone', + ] as $requiredAttribute + ) { + $this->assertArrayHasKey($requiredAttribute, $data[0]); + } + + return $this->response; + }); + $controller->shifts(); + } + public function getNewsMeetingsDataProvider(): array { diff --git a/tests/Unit/Models/Shifts/ShiftEntryTest.php b/tests/Unit/Models/Shifts/ShiftEntryTest.php index af47c44d..80210553 100644 --- a/tests/Unit/Models/Shifts/ShiftEntryTest.php +++ b/tests/Unit/Models/Shifts/ShiftEntryTest.php @@ -33,5 +33,7 @@ class ShiftEntryTest extends ModelTest $this->assertEquals($shift->id, $model->shift->id); $this->assertEquals($angelType->id, $model->angelType->id); $this->assertEquals($user->id, $model->user->id); + + $this->assertArrayNotHasKey('freeloaded_comment', $model->toArray()); } }