From 1883ff138042dce42abaf541fe777662190904c2 Mon Sep 17 00:00:00 2001 From: Martin Chaine Date: Tue, 3 Jun 2025 18:45:01 +0200 Subject: [PATCH] refuse requests for deletions records that are too old --- .../Api/EntryDeletionRestController.php | 47 +++++++++++++++++-- .../Api/EntryDeletionRestControllerTest.php | 20 +++++++- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/Controller/Api/EntryDeletionRestController.php b/src/Controller/Api/EntryDeletionRestController.php index 65f1593f6..7ec58197c 100644 --- a/src/Controller/Api/EntryDeletionRestController.php +++ b/src/Controller/Api/EntryDeletionRestController.php @@ -7,8 +7,10 @@ use Hateoas\Representation\Factory\PagerfantaFactory; use OpenApi\Attributes as OA; use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\GoneHttpException; use Symfony\Component\Routing\Annotation\Route; use Wallabag\Entity\EntryDeletion; +use Wallabag\Helper\EntryDeletionExpirationConfig; use Wallabag\Repository\EntryDeletionRepository; use Wallabag\OpenApi\Attribute as WOA; @@ -36,26 +38,63 @@ class EntryDeletionRestController extends WallabagRestController responses: [ new OA\Response( response: 200, - description: 'Returned when successful', + description: 'Returned when successful.', content: new WOA\PagerFanta\JsonContent(EntryDeletion::class) + ), + new OA\Response( + response: 410, + description: 'Returned when the since date is before the cutoff date.', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'message' => new OA\Property(type: 'string'), + ] + ), + headers: [ + new OA\Header( + header: 'X-Wallabag-Entry-Deletion-Cutoff', + description: 'The furthest cutoff timestamp possible for entry deletions.', + schema: new OA\Schema(type: 'integer') + ), + ] ) ] )] #[IsGranted('LIST_ENTRIES')] - public function getEntryDeletionsAction(Request $request, EntryDeletionRepository $entryDeletionRepository) - { + public function getEntryDeletionsAction( + Request $request, + EntryDeletionRepository $entryDeletionRepository, + EntryDeletionExpirationConfig $expirationConfig + ) { $this->validateAuthentication(); $userId = $this->getUser()->getId(); $page = $request->query->get('page', 1); $perPage = $request->query->get('perPage', 100); $order = $request->query->get('order', 'desc'); - $since = $request->query->get('since'); + $since = (int)$request->query->get('since', 0); if (!\in_array($order, ['asc', 'desc'], true)) { $order = 'desc'; } + if (0 < $since) { + $cutoff = $expirationConfig->getCutoffDate()->getTimestamp(); + if ($since < $cutoff) { + // Using a JSON response rather than a GoneHttpException to preserve the error message in production + return $this->json( + [ + 'message' => "The requested since date ({$since}) is before the data retention cutoff date ({$cutoff}).\n" + . "You can get the cutoff date programmatically from the X-Wallabag-Entry-Deletion-Cutoff header.", + ], + 410, + headers: [ + 'X-Wallabag-Entry-Deletion-Cutoff' => $cutoff, + ] + ); + } + } + $pager = $entryDeletionRepository->findEntryDeletions($userId, $since, $order); $pager->setMaxPerPage($perPage); $pager->setCurrentPage($page); diff --git a/tests/Controller/Api/EntryDeletionRestControllerTest.php b/tests/Controller/Api/EntryDeletionRestControllerTest.php index 941711c3a..f2a5359b5 100644 --- a/tests/Controller/Api/EntryDeletionRestControllerTest.php +++ b/tests/Controller/Api/EntryDeletionRestControllerTest.php @@ -33,7 +33,6 @@ class EntryDeletionRestControllerTest extends WallabagApiTestCase public function testGetEntryDeletionsSince() { - // Test date range filter - get deletions from last 2 days $since = (new \DateTime('-2 days'))->getTimestamp(); $this->client->request('GET', "/api/entry-deletions?since={$since}"); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); @@ -41,4 +40,23 @@ class EntryDeletionRestControllerTest extends WallabagApiTestCase $content = json_decode($this->client->getResponse()->getContent(), true); $this->assertGreaterThanOrEqual(1, \count($content['_embedded']['items'])); } + + public function testGetEntryDeletionsWithSinceBeforeCutoffDate() + { + $sinceBeforeCutoff = (new \DateTime('-410 days'))->getTimestamp(); + $this->client->request('GET', "/api/entry-deletions?since={$sinceBeforeCutoff}"); + $this->assertSame(410, $this->client->getResponse()->getStatusCode()); + + $content = $this->client->getResponse()->getContent(); + $this->assertStringContainsString('The requested since date', $content); + $this->assertStringContainsString('is before the data retention cutoff date', $content); + $this->assertStringContainsString('X-Wallabag-Entry-Deletion-Cutoff', $content); + + $response = $this->client->getResponse(); + $this->assertTrue($response->headers->has('X-Wallabag-Entry-Deletion-Cutoff')); + + $cutoffTimestamp = $response->headers->get('X-Wallabag-Entry-Deletion-Cutoff'); + $this->assertIsNumeric($cutoffTimestamp); + $this->assertGreaterThan($sinceBeforeCutoff, (int) $cutoffTimestamp); + } }