diff --git a/composer.json b/composer.json index 5b8e2a7..ba5526c 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-main", + "phplist/core": "dev-dev", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", "symfony/process": "^6.4", diff --git a/config/services/normalizers.yml b/config/services/normalizers.yml index 1d16cc3..a270a3f 100644 --- a/config/services/normalizers.yml +++ b/config/services/normalizers.yml @@ -93,3 +93,7 @@ services: PhpList\RestBundle\Statistics\Serializer\TopLocalPartsNormalizer: tags: [ 'serializer.normalizer' ] autowire: true + + PhpList\RestBundle\Subscription\Serializer\UserBlacklistNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true diff --git a/src/Subscription/Controller/BlacklistController.php b/src/Subscription/Controller/BlacklistController.php new file mode 100644 index 0000000..f76b094 --- /dev/null +++ b/src/Subscription/Controller/BlacklistController.php @@ -0,0 +1,284 @@ +authentication = $authentication; + $this->blacklistManager = $blacklistManager; + $this->normalizer = $normalizer; + } + + #[Route('/check/{email}', name: 'check', methods: ['GET'])] + #[OA\Get( + path: '/api/v2/blacklist/check/{email}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Check if email is blacklisted', + tags: ['blacklist'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'email', + description: 'Email address to check', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'blacklisted', type: 'boolean'), + new OA\Property(property: 'reason', type: 'string', nullable: true) + ] + ), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + ] + )] + public function checkEmailBlacklisted(Request $request, string $email): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to check blacklisted emails.'); + } + + $isBlacklisted = $this->blacklistManager->isEmailBlacklisted($email); + $reason = $isBlacklisted ? $this->blacklistManager->getBlacklistReason($email) : null; + + return $this->json([ + 'blacklisted' => $isBlacklisted, + 'reason' => $reason, + ]); + } + + #[Route('/add', name: 'add', methods: ['POST'])] + #[OA\Post( + path: '/api/v2/blacklist/add', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Adds an email address to the blacklist.', + requestBody: new OA\RequestBody( + description: 'Email to blacklist', + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'email', type: 'string'), + new OA\Property(property: 'reason', type: 'string', nullable: true) + ] + ) + ), + tags: ['blacklist'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 201, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'success', type: 'boolean'), + new OA\Property(property: 'message', type: 'string') + ] + ), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 422, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse') + ), + ] + )] + public function addEmailToBlacklist(Request $request): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to add emails to blacklist.'); + } + + /** @var AddToBlacklistRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, AddToBlacklistRequest::class); + + $userBlacklisted = $this->blacklistManager->addEmailToBlacklist( + email: $definitionRequest->email, + reasonData: $definitionRequest->reason + ); + $json = $this->normalizer->normalize($userBlacklisted, 'json'); + + return $this->json($json, Response::HTTP_CREATED); + } + + #[Route('/remove/{email}', name: 'remove', methods: ['DELETE'])] + #[OA\Delete( + path: '/api/v2/blacklist/remove/{email}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Removes an email address from the blacklist.', + tags: ['blacklist'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'email', + description: 'Email address to remove from blacklist', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'success', type: 'boolean'), + new OA\Property(property: 'message', type: 'string') + ] + ), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + ] + )] + public function removeEmailFromBlacklist(Request $request, string $email): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to remove emails from blacklist.'); + } + + $this->blacklistManager->removeEmailFromBlacklist($email); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route('/info/{email}', name: 'info', methods: ['GET'])] + #[OA\Get( + path: '/api/v2/blacklist/info/{email}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Gets detailed information about a blacklisted email.', + tags: ['blacklist'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'email', + description: 'Email address to get information for', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'email', type: 'string'), + new OA\Property(property: 'added', type: 'string', format: 'date-time', nullable: true), + new OA\Property(property: 'reason', type: 'string', nullable: true) + ] + ), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/BadRequestResponse') + ), + ] + )] + public function getBlacklistInfo(Request $request, string $email): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to view blacklist information.'); + } + + $blacklistInfo = $this->blacklistManager->getBlacklistInfo($email); + if (!$blacklistInfo) { + return $this->json([ + 'error' => sprintf('Email %s is not blacklisted', $email) + ], Response::HTTP_NOT_FOUND); + } + + $reason = $this->blacklistManager->getBlacklistReason($email); + + return $this->json([ + 'email' => $blacklistInfo->getEmail(), + 'added' => $blacklistInfo->getAdded()?->format('c'), + 'reason' => $reason, + ]); + } +} diff --git a/src/Subscription/Request/AddToBlacklistRequest.php b/src/Subscription/Request/AddToBlacklistRequest.php new file mode 100644 index 0000000..68dc81e --- /dev/null +++ b/src/Subscription/Request/AddToBlacklistRequest.php @@ -0,0 +1,23 @@ +blacklistManager->getBlacklistReason($object->getEmail()); + + return [ + 'email' => $object->getEmail(), + 'added' => $object->getAdded()?->format('Y-m-d\TH:i:sP'), + 'reason' => $reason, + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof UserBlacklist; + } +} diff --git a/tests/Integration/Subscription/Controller/BlacklistControllerTest.php b/tests/Integration/Subscription/Controller/BlacklistControllerTest.php new file mode 100644 index 0000000..fdba52a --- /dev/null +++ b/tests/Integration/Subscription/Controller/BlacklistControllerTest.php @@ -0,0 +1,60 @@ +get(BlacklistController::class) + ); + } + + public function testCheckEmailBlacklistedWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('get', '/api/v2/blacklist/check/test@example.com'); + + $this->assertHttpForbidden(); + } + + public function testAddEmailToBlacklistWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('post', '/api/v2/blacklist/add'); + + $this->assertHttpForbidden(); + } + + public function testAddEmailToBlacklistWithMissingEmailReturnsUnprocessableEntityStatus(): void + { + $jsonData = []; + + $this->authenticatedJsonRequest('post', '/api/v2/blacklist/add', [], [], [], json_encode($jsonData)); + + $this->assertHttpUnprocessableEntity(); + } + + public function testRemoveEmailFromBlacklistWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('delete', '/api/v2/blacklist/remove/test@example.com'); + + $this->assertHttpForbidden(); + } + + public function testGetBlacklistInfoWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('get', '/api/v2/blacklist/info/test@example.com'); + + $this->assertHttpForbidden(); + } +} diff --git a/tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php b/tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php new file mode 100644 index 0000000..85619b6 --- /dev/null +++ b/tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php @@ -0,0 +1,57 @@ +createMock(SubscriberBlacklistManager::class); + $normalizer = new UserBlacklistNormalizer($blacklistManager); + $userBlacklist = $this->createMock(UserBlacklist::class); + + $this->assertTrue($normalizer->supportsNormalization($userBlacklist)); + $this->assertFalse($normalizer->supportsNormalization(new stdClass())); + } + + public function testNormalize(): void + { + $email = 'test@example.com'; + $added = new DateTime('2025-08-08T12:00:00+00:00'); + $reason = 'Unsubscribed by user'; + + $userBlacklist = $this->createMock(UserBlacklist::class); + $userBlacklist->method('getEmail')->willReturn($email); + $userBlacklist->method('getAdded')->willReturn($added); + + $blacklistManager = $this->createMock(SubscriberBlacklistManager::class); + $blacklistManager->method('getBlacklistReason')->with($email)->willReturn($reason); + + $normalizer = new UserBlacklistNormalizer($blacklistManager); + + $expected = [ + 'email' => $email, + 'added' => '2025-08-08T12:00:00+00:00', + 'reason' => $reason, + ]; + + $this->assertSame($expected, $normalizer->normalize($userBlacklist)); + } + + public function testNormalizeWithInvalidObject(): void + { + $blacklistManager = $this->createMock(SubscriberBlacklistManager::class); + $normalizer = new UserBlacklistNormalizer($blacklistManager); + + $this->assertSame([], $normalizer->normalize(new stdClass())); + } +}