diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..b354963b --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "en-US" +tone_instructions: "chill" +reviews: + profile: "chill" + high_level_summary: true + collapse_walkthrough: true + suggested_labels: false + high_level_summary_in_walkthrough: false + changed_files_summary: false + poem: false + auto_review: + enabled: true + base_branches: + - ".*" + drafts: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b4c9b3f7..aad3619f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,37 +1,3 @@ -### Summary - -Provide a general description of the code changes in your pull request … -were there any bugs you had fixed? If so, mention them. If these bugs have open -GitHub issues, be sure to tag them here as well, to keep the conversation -linked together. - - -### Unit test - -Are your changes covered with unit tests, and do they not break anything? - -You can run the existing unit tests using this command: - - vendor/bin/phpunit tests/ - - -### Code style - -Have you checked that you code is well-documented and follows the PSR-2 coding -style? - -You can check for this using this command: - - vendor/bin/phpcs --standard=PSR2 src/ tests/ - - -### Other Information - -If there's anything else that's important and relevant to your pull -request, mention that information here. This could include benchmarks, -or other information. - -If you are updating any of the CHANGELOG files or are asked to update the -CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the file. +"@coderabbitai summary" Thanks for contributing to phpList! diff --git a/composer.json b/composer.json index 5b8e2a73..c4c880f8 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,12 @@ "role": "Maintainer" } ], + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/TatevikGr/rss-bundle.git" + } + ], "support": { "issues": "https://github.com/phpList/rest-api/issues", "forum": "https://discuss.phplist.org/", @@ -41,7 +47,8 @@ "symfony/test-pack": "^1.0", "symfony/process": "^6.4", "zircote/swagger-php": "^4.11", - "ext-dom": "*" + "ext-dom": "*", + "tatevikgr/rss-feed": "dev-main as 0.1.0" }, "require-dev": { "phpunit/phpunit": "^10.0", @@ -123,5 +130,10 @@ } } } + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } } } diff --git a/config/services/managers.yml b/config/services/managers.yml index 37f0b029..99253992 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -20,15 +20,19 @@ services: autowire: true autoconfigure: true - PhpList\Core\Domain\Messaging\Service\MessageManager: + PhpList\Core\Domain\Messaging\Service\Manager\MessageManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Messaging\Service\TemplateManager: + PhpList\Core\Domain\Messaging\Service\Manager\TemplateManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Messaging\Service\TemplateImageManager: + PhpList\Core\Domain\Messaging\Service\Manager\TemplateImageManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\Service\Manager\BounceRegexManager: autowire: true autoconfigure: true diff --git a/config/services/messenger_handlers.yml b/config/services/messenger_handlers.yml new file mode 100644 index 00000000..9073d52f --- /dev/null +++ b/config/services/messenger_handlers.yml @@ -0,0 +1,5 @@ +services: + PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessorMessageHandler: + autowire: true + autoconfigure: true + public: false diff --git a/config/services/normalizers.yml b/config/services/normalizers.yml index 1d16cc3f..2179f6ad 100644 --- a/config/services/normalizers.yml +++ b/config/services/normalizers.yml @@ -93,3 +93,15 @@ services: PhpList\RestBundle\Statistics\Serializer\TopLocalPartsNormalizer: tags: [ 'serializer.normalizer' ] autowire: true + + PhpList\RestBundle\Subscription\Serializer\UserBlacklistNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true + + PhpList\RestBundle\Subscription\Serializer\SubscribePageNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true + + PhpList\RestBundle\Messaging\Serializer\BounceRegexNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true diff --git a/src/Identity/Controller/AdminAttributeDefinitionController.php b/src/Identity/Controller/AdminAttributeDefinitionController.php index 772a56d1..ff4c8531 100644 --- a/src/Identity/Controller/AdminAttributeDefinitionController.php +++ b/src/Identity/Controller/AdminAttributeDefinitionController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\AdminAttributeDefinition; use PhpList\Core\Domain\Identity\Service\AdminAttributeDefinitionManager; @@ -32,7 +33,8 @@ public function __construct( RequestValidator $validator, AdminAttributeDefinitionManager $definitionManager, AdminAttributeDefinitionNormalizer $normalizer, - PaginatedDataProvider $paginatedDataProvider + PaginatedDataProvider $paginatedDataProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->definitionManager = $definitionManager; @@ -89,6 +91,8 @@ public function create(Request $request): JsonResponse $definitionRequest = $this->validator->validate($request, CreateAttributeDefinitionRequest::class); $attributeDefinition = $this->definitionManager->create($definitionRequest->getDto()); + $this->entityManager->flush(); + $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_CREATED); @@ -156,6 +160,7 @@ public function update( attributeDefinition: $attributeDefinition, attributeDefinitionDto: $definitionRequest->getDto(), ); + $this->entityManager->flush(); $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_OK); @@ -211,6 +216,7 @@ public function delete( } $this->definitionManager->delete($attributeDefinition); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Identity/Controller/AdminAttributeValueController.php b/src/Identity/Controller/AdminAttributeValueController.php index ca897234..573608ba 100644 --- a/src/Identity/Controller/AdminAttributeValueController.php +++ b/src/Identity/Controller/AdminAttributeValueController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\Filter\AdminAttributeValueFilter; use PhpList\Core\Domain\Identity\Model\Administrator; @@ -27,18 +28,21 @@ class AdminAttributeValueController extends BaseController private AdminAttributeManager $attributeManager; private AdminAttributeValueNormalizer $normalizer; private PaginatedDataProvider $paginatedDataProvider; + private EntityManagerInterface $entityManager; public function __construct( Authentication $authentication, RequestValidator $validator, AdminAttributeManager $attributeManager, AdminAttributeValueNormalizer $normalizer, - PaginatedDataProvider $paginatedDataProvider + PaginatedDataProvider $paginatedDataProvider, + EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->attributeManager = $attributeManager; $this->normalizer = $normalizer; $this->paginatedDataProvider = $paginatedDataProvider; + $this->entityManager = $entityManager; } #[Route( @@ -122,6 +126,7 @@ public function createOrUpdate( definition: $definition, value: $request->toArray()['value'] ?? null ); + $this->entityManager->flush(); $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_CREATED); @@ -193,6 +198,7 @@ public function delete( throw $this->createNotFoundException('Administrator attribute not found.'); } $this->attributeManager->delete($attribute); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -350,6 +356,7 @@ public function getAttributeDefinition( attributeDefinitionId: $definition->getId() ); $this->attributeManager->delete($attribute); + $this->entityManager->flush(); return $this->json( $this->normalizer->normalize($attribute), diff --git a/src/Identity/Controller/AdministratorController.php b/src/Identity/Controller/AdministratorController.php index 77e9288b..365cf125 100644 --- a/src/Identity/Controller/AdministratorController.php +++ b/src/Identity/Controller/AdministratorController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Identity\Service\AdministratorManager; @@ -35,7 +36,8 @@ public function __construct( RequestValidator $validator, AdministratorManager $administratorManager, AdministratorNormalizer $normalizer, - PaginatedDataProvider $paginatedProvider + PaginatedDataProvider $paginatedProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->administratorManager = $administratorManager; @@ -149,6 +151,7 @@ public function createAdministrator( $createRequest = $validator->validate($request, CreateAdministratorRequest::class); $administrator = $this->administratorManager->createAdministrator($createRequest->getDto()); + $this->entityManager->flush(); $json = $normalizer->normalize($administrator, 'json'); return $this->json($json, Response::HTTP_CREATED); @@ -255,6 +258,7 @@ public function updateAdministrator( /** @var UpdateAdministratorRequest $updateRequest */ $updateRequest = $this->validator->validate($request, UpdateAdministratorRequest::class); $this->administratorManager->updateAdministrator($administrator, $updateRequest->getDto()); + $this->entityManager->flush(); return $this->json($this->normalizer->normalize($administrator), Response::HTTP_OK); } @@ -303,6 +307,7 @@ public function deleteAdministrator( throw $this->createNotFoundException('Administrator not found.'); } $this->administratorManager->deleteAdministrator($administrator); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Identity/Controller/PasswordResetController.php b/src/Identity/Controller/PasswordResetController.php index de5d3d67..a3527b07 100644 --- a/src/Identity/Controller/PasswordResetController.php +++ b/src/Identity/Controller/PasswordResetController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Service\PasswordManager; use PhpList\Core\Security\Authentication; @@ -29,6 +30,7 @@ public function __construct( Authentication $authentication, RequestValidator $validator, PasswordManager $passwordManager, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); @@ -74,6 +76,7 @@ public function requestPasswordReset(Request $request): JsonResponse $resetRequest = $this->validator->validate($request, RequestPasswordResetRequest::class); $this->passwordManager->generatePasswordResetToken($resetRequest->email); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -117,6 +120,7 @@ public function validateToken(Request $request): JsonResponse $validateRequest = $this->validator->validate($request, ValidateTokenRequest::class); $administrator = $this->passwordManager->validatePasswordResetToken($validateRequest->token); + $this->entityManager->flush(); return $this->json([ 'valid' => $administrator !== null]); } @@ -169,6 +173,7 @@ public function resetPassword(Request $request): JsonResponse $resetRequest->token, $resetRequest->newPassword ); + $this->entityManager->flush(); if ($success) { return $this->json([ 'message' => 'Password updated successfully']); diff --git a/src/Identity/Controller/SessionController.php b/src/Identity/Controller/SessionController.php index 5f588119..66b49ce9 100644 --- a/src/Identity/Controller/SessionController.php +++ b/src/Identity/Controller/SessionController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\AdministratorToken; use PhpList\Core\Domain\Identity\Service\SessionManager; @@ -34,6 +35,7 @@ public function __construct( Authentication $authentication, RequestValidator $validator, SessionManager $sessionManager, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); @@ -96,6 +98,7 @@ public function createSession( loginName:$createSessionRequest->loginName, password: $createSessionRequest->password ); + $this->entityManager->flush(); $json = $normalizer->normalize($token, 'json'); @@ -163,6 +166,7 @@ public function deleteSession( } $this->sessionManager->deleteSession($token); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Identity/Request/CreateSessionRequest.php b/src/Identity/Request/CreateSessionRequest.php index fb61b094..abff868a 100644 --- a/src/Identity/Request/CreateSessionRequest.php +++ b/src/Identity/Request/CreateSessionRequest.php @@ -9,11 +9,11 @@ class CreateSessionRequest implements RequestInterface { - #[Assert\NotBlank] + #[Assert\NotBlank(normalizer: 'trim')] #[Assert\Type(type: 'string')] public string $loginName; - #[Assert\NotBlank] + #[Assert\NotBlank(normalizer: 'trim')] #[Assert\Type(type: 'string')] public string $password; diff --git a/src/Identity/Request/UpdateAttributeDefinitionRequest.php b/src/Identity/Request/UpdateAttributeDefinitionRequest.php index 7313a905..8f387392 100644 --- a/src/Identity/Request/UpdateAttributeDefinitionRequest.php +++ b/src/Identity/Request/UpdateAttributeDefinitionRequest.php @@ -10,7 +10,7 @@ class UpdateAttributeDefinitionRequest implements RequestInterface { - #[Assert\NotBlank] + #[Assert\NotBlank(normalizer: 'trim')] public string $name; public ?string $type = null; diff --git a/src/Identity/Request/ValidateTokenRequest.php b/src/Identity/Request/ValidateTokenRequest.php index 481b95fc..ffeb0f84 100644 --- a/src/Identity/Request/ValidateTokenRequest.php +++ b/src/Identity/Request/ValidateTokenRequest.php @@ -9,7 +9,7 @@ class ValidateTokenRequest implements RequestInterface { - #[Assert\NotBlank] + #[Assert\NotBlank(normalizer: 'trim')] #[Assert\Type(type: 'string')] public string $token; diff --git a/src/Messaging/Controller/BounceRegexController.php b/src/Messaging/Controller/BounceRegexController.php new file mode 100644 index 00000000..c9e9a1cb --- /dev/null +++ b/src/Messaging/Controller/BounceRegexController.php @@ -0,0 +1,250 @@ +requireAuthentication($request); + $items = $this->manager->getAll(); + $normalized = array_map(fn($bounceRegex) => $this->normalizer->normalize($bounceRegex), $items); + + return $this->json($normalized, Response::HTTP_OK); + } + + #[Route('/{regexHash}', name: 'get_one', methods: ['GET'])] + #[OA\Get( + path: '/api/v2/bounces/regex/{regexHash}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a bounce regex by its hash.', + summary: 'Get a bounce regex by its hash', + tags: ['bounces'], + 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: 'regexHash', + description: 'Regex hash', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/BounceRegex') + ), + 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/NotFoundErrorResponse') + ) + ] + )] + public function getOne(Request $request, string $regexHash): JsonResponse + { + $this->requireAuthentication($request); + $entity = $this->manager->getByHash($regexHash); + if (!$entity) { + throw $this->createNotFoundException('Bounce regex not found.'); + } + + return $this->json($this->normalizer->normalize($entity), Response::HTTP_OK); + } + + #[Route('', name: 'create_or_update', methods: ['POST'])] + #[OA\Post( + path: '/api/v2/bounces/regex', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Creates a new bounce regex or updates an existing one (matched by regex hash).', + summary: 'Create or update a bounce regex', + requestBody: new OA\RequestBody( + description: 'Create or update a bounce regex rule.', + required: true, + content: new OA\JsonContent( + required: ['regex'], + properties: [ + new OA\Property(property: 'regex', type: 'string', example: '/mailbox is full/i'), + new OA\Property(property: 'action', type: 'string', example: 'delete', nullable: true), + new OA\Property(property: 'list_order', type: 'integer', example: 0, nullable: true), + new OA\Property(property: 'admin', type: 'integer', example: 1, nullable: true), + new OA\Property(property: 'comment', type: 'string', example: 'Auto-generated', nullable: true), + new OA\Property(property: 'status', type: 'string', example: 'active', nullable: true), + ], + type: 'object' + ) + ), + tags: ['bounces'], + 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(ref: '#/components/schemas/BounceRegex') + ), + 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 createOrUpdate(Request $request): JsonResponse + { + $this->requireAuthentication($request); + /** @var CreateBounceRegexRequest $dto */ + $dto = $this->validator->validate($request, CreateBounceRegexRequest::class); + + $entity = $this->manager->createOrUpdateFromPattern( + regex: $dto->regex, + action: $dto->action, + listOrder: $dto->listOrder, + adminId: $dto->admin, + comment: $dto->comment, + status: $dto->status + ); + $this->entityManager->flush(); + + return $this->json($this->normalizer->normalize($entity), Response::HTTP_CREATED); + } + + #[Route('/{regexHash}', name: 'delete', methods: ['DELETE'])] + #[OA\Delete( + path: '/api/v2/bounces/regex/{regexHash}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Delete a bounce regex by its hash.', + summary: 'Delete a bounce regex by its hash', + tags: ['bounces'], + 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: 'regexHash', + description: 'Regex hash', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_NO_CONTENT, + description: 'Success' + ), + 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/NotFoundErrorResponse') + ) + ] + )] + public function delete(Request $request, string $regexHash): JsonResponse + { + $this->requireAuthentication($request); + $entity = $this->manager->getByHash($regexHash); + if (!$entity) { + throw $this->createNotFoundException('Bounce regex not found.'); + } + $this->manager->delete($entity); + $this->entityManager->flush(); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Messaging/Controller/CampaignController.php b/src/Messaging/Controller/CampaignController.php index 6ebac37a..b1b8e804 100644 --- a/src/Messaging/Controller/CampaignController.php +++ b/src/Messaging/Controller/CampaignController.php @@ -4,9 +4,10 @@ namespace PhpList\RestBundle\Messaging\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; +use PhpList\Core\Domain\Messaging\Message\SyncCampaignProcessorMessage; use PhpList\Core\Domain\Messaging\Model\Message; -use PhpList\Core\Domain\Messaging\Service\CampaignProcessor; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Validator\RequestValidator; @@ -17,6 +18,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Attribute\Route; /** @@ -28,17 +30,18 @@ class CampaignController extends BaseController { private CampaignService $campaignService; - private CampaignProcessor $campaignProcessor; + private MessageBusInterface $messageBus; public function __construct( Authentication $authentication, RequestValidator $validator, CampaignService $campaignService, - CampaignProcessor $campaignProcessor, + MessageBusInterface $messageBus, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->campaignService = $campaignService; - $this->campaignProcessor = $campaignProcessor; + $this->messageBus = $messageBus; } #[Route('', name: 'get_list', methods: ['GET'])] @@ -211,11 +214,10 @@ public function createMessage(Request $request): JsonResponse /** @var CreateMessageRequest $createMessageRequest */ $createMessageRequest = $this->validator->validate($request, CreateMessageRequest::class); + $message = $this->campaignService->createMessage($createMessageRequest, $authUser); + $this->entityManager->flush(); - return $this->json( - $this->campaignService->createMessage($createMessageRequest, $authUser), - Response::HTTP_CREATED - ); + return $this->json(data: $message, status: Response::HTTP_CREATED); } #[Route('/{messageId}', name: 'update', requirements: ['messageId' => '\d+'], methods: ['PUT'])] @@ -284,11 +286,10 @@ public function updateMessage( /** @var UpdateMessageRequest $updateMessageRequest */ $updateMessageRequest = $this->validator->validate($request, UpdateMessageRequest::class); + $message = $this->campaignService->updateMessage($updateMessageRequest, $authUser, $message); + $this->entityManager->flush(); - return $this->json( - $this->campaignService->updateMessage($updateMessageRequest, $authUser, $message), - Response::HTTP_OK - ); + return $this->json(data:$message, status: Response::HTTP_OK); } #[Route('/{messageId}', name: 'delete', requirements: ['messageId' => '\d+'], methods: ['DELETE'])] @@ -339,6 +340,7 @@ public function deleteMessage( $authUser = $this->requireAuthentication($request); $this->campaignService->deleteMessage($authUser, $message); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -388,7 +390,7 @@ public function sendMessage( throw $this->createNotFoundException('Campaign not found.'); } - $this->campaignProcessor->process($message); + $this->messageBus->dispatch(new SyncCampaignProcessorMessage($message->getId())); return $this->json($this->campaignService->getMessage($message), Response::HTTP_OK); } diff --git a/src/Messaging/Controller/ListMessageController.php b/src/Messaging/Controller/ListMessageController.php index 0ee3eb9e..f67c8fa9 100644 --- a/src/Messaging/Controller/ListMessageController.php +++ b/src/Messaging/Controller/ListMessageController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Messaging\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Messaging\Service\Manager\ListMessageManager; @@ -37,7 +38,8 @@ public function __construct( ListMessageManager $listMessageManager, ListMessageNormalizer $listMessageNormalizer, SubscriberListNormalizer $subscriberListNormalizer, - MessageNormalizer $messageNormalizer + MessageNormalizer $messageNormalizer, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->listMessageManager = $listMessageManager; @@ -262,6 +264,7 @@ public function associateMessageWithList( } $listMessage = $this->listMessageManager->associateMessageWithList($message, $subscriberList); + $this->entityManager->flush(); return $this->json( data: $this->listMessageNormalizer->normalize($listMessage), diff --git a/src/Messaging/Controller/TemplateController.php b/src/Messaging/Controller/TemplateController.php index 6513db26..bc24a02d 100644 --- a/src/Messaging/Controller/TemplateController.php +++ b/src/Messaging/Controller/TemplateController.php @@ -4,9 +4,10 @@ namespace PhpList\RestBundle\Messaging\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Messaging\Model\Template; -use PhpList\Core\Domain\Messaging\Service\TemplateManager; +use PhpList\Core\Domain\Messaging\Service\Manager\TemplateManager; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; @@ -37,6 +38,7 @@ public function __construct( TemplateNormalizer $normalizer, TemplateManager $templateManager, PaginatedDataProvider $paginatedDataProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->normalizer = $normalizer; @@ -260,9 +262,11 @@ public function createTemplates(Request $request): JsonResponse /** @var CreateTemplateRequest $createTemplateRequest */ $createTemplateRequest = $this->validator->validate($request, CreateTemplateRequest::class); + $template = $this->templateManager->create($createTemplateRequest->getDto()); + $this->entityManager->flush(); return $this->json( - $this->normalizer->normalize($this->templateManager->create($createTemplateRequest->getDto())), + $this->normalizer->normalize($template), Response::HTTP_CREATED ); } @@ -318,6 +322,7 @@ public function delete( } $this->templateManager->delete($template); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Messaging/OpenApi/SwaggerSchemasResponse.php b/src/Messaging/OpenApi/SwaggerSchemasResponse.php index a14e9b58..9e4cfb55 100644 --- a/src/Messaging/OpenApi/SwaggerSchemasResponse.php +++ b/src/Messaging/OpenApi/SwaggerSchemasResponse.php @@ -120,6 +120,21 @@ ], type: 'object' )] +#[OA\Schema( + schema: 'BounceRegex', + properties: [ + new OA\Property(property: 'id', type: 'integer', example: 10), + new OA\Property(property: 'regex', type: 'string', example: '/mailbox is full/i'), + new OA\Property(property: 'regex_hash', type: 'string', example: 'd41d8cd98f00b204e9800998ecf8427e'), + new OA\Property(property: 'action', type: 'string', example: 'delete', nullable: true), + new OA\Property(property: 'list_order', type: 'integer', example: 0, nullable: true), + new OA\Property(property: 'admin_id', type: 'integer', example: 1, nullable: true), + new OA\Property(property: 'comment', type: 'string', example: 'Auto-generated rule', nullable: true), + new OA\Property(property: 'status', type: 'string', example: 'active', nullable: true), + new OA\Property(property: 'count', type: 'integer', example: 5, nullable: true), + ], + type: 'object' +)] class SwaggerSchemasResponse { } diff --git a/src/Messaging/Request/CreateBounceRegexRequest.php b/src/Messaging/Request/CreateBounceRegexRequest.php new file mode 100644 index 00000000..dfbee9a3 --- /dev/null +++ b/src/Messaging/Request/CreateBounceRegexRequest.php @@ -0,0 +1,63 @@ + $this->regex, + 'action' => $this->action, + 'listOrder' => $this->listOrder, + 'admin' => $this->admin, + 'comment' => $this->comment, + 'status' => $this->status, + ]; + } + + #[Assert\Callback('validateRegexPattern')] + public function validateRegexPattern(ExecutionContextInterface $context): void + { + if (!isset($this->regex)) { + return; + } + set_error_handler(static function () { + return true; + }); + // phpcs:ignore Generic.PHP.NoSilencedErrors + $allGood = @preg_match($this->regex, ''); + restore_error_handler(); + + if ($allGood === false) { + $context->buildViolation('Invalid regular expression pattern.') + ->atPath('regex') + ->addViolation(); + } + } +} diff --git a/src/Messaging/Request/CreateTemplateRequest.php b/src/Messaging/Request/CreateTemplateRequest.php index c5bbbcb8..9ab4c6c8 100644 --- a/src/Messaging/Request/CreateTemplateRequest.php +++ b/src/Messaging/Request/CreateTemplateRequest.php @@ -6,20 +6,21 @@ use PhpList\Core\Domain\Messaging\Model\Dto\CreateTemplateDto; use PhpList\RestBundle\Common\Request\RequestInterface; +use PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholder; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraints as Assert; class CreateTemplateRequest implements RequestInterface { - #[Assert\NotBlank] + #[Assert\NotBlank(normalizer: 'trim')] #[Assert\NotNull] public string $title; #[Assert\NotBlank] - #[\PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholder] + #[ContainsPlaceholder] public string $content; - #[\PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholder] + #[ContainsPlaceholder] public ?string $text = null; public ?UploadedFile $file = null; diff --git a/src/Messaging/Request/Message/MessageMetadataRequest.php b/src/Messaging/Request/Message/MessageMetadataRequest.php index ca908e62..03fd3323 100644 --- a/src/Messaging/Request/Message/MessageMetadataRequest.php +++ b/src/Messaging/Request/Message/MessageMetadataRequest.php @@ -5,6 +5,7 @@ namespace PhpList\RestBundle\Messaging\Request\Message; use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageMetadataDto; +use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus; use Symfony\Component\Validator\Constraints as Assert; class MessageMetadataRequest implements RequestDtoInterface @@ -12,10 +13,13 @@ class MessageMetadataRequest implements RequestDtoInterface #[Assert\NotBlank] public string $status; + /** + * @SuppressWarnings(PHPMD.StaticAccess) + */ public function getDto(): MessageMetadataDto { return new MessageMetadataDto( - status: $this->status, + status: MessageStatus::from($this->status), ); } } diff --git a/src/Messaging/Serializer/BounceRegexNormalizer.php b/src/Messaging/Serializer/BounceRegexNormalizer.php new file mode 100644 index 00000000..5771bd8b --- /dev/null +++ b/src/Messaging/Serializer/BounceRegexNormalizer.php @@ -0,0 +1,41 @@ + $object->getId(), + 'regex' => $object->getRegex(), + 'regex_hash' => $object->getRegexHash(), + 'action' => $object->getAction(), + 'list_order' => $object->getListOrder(), + 'admin_id' => $object->getAdminId(), + 'comment' => $object->getComment(), + 'status' => $object->getStatus(), + 'count' => $object->getCount(), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof BounceRegex; + } +} diff --git a/src/Messaging/Serializer/MessageNormalizer.php b/src/Messaging/Serializer/MessageNormalizer.php index b659b3d6..dcad6358 100644 --- a/src/Messaging/Serializer/MessageNormalizer.php +++ b/src/Messaging/Serializer/MessageNormalizer.php @@ -39,7 +39,7 @@ public function normalize($object, string $format = null, array $context = []): 'format_options' => $object->getFormat()->getFormatOptions() ], 'message_metadata' => [ - 'status' => $object->getMetadata()->getStatus(), + 'status' => $object->getMetadata()->getStatus()->value, 'processed' => $object->getMetadata()->isProcessed(), 'views' => $object->getMetadata()->getViews(), 'bounce_count' => $object->getMetadata()->getBounceCount(), diff --git a/src/Messaging/Service/CampaignService.php b/src/Messaging/Service/CampaignService.php index b6680d1e..4bc9e3c6 100644 --- a/src/Messaging/Service/CampaignService.php +++ b/src/Messaging/Service/CampaignService.php @@ -4,11 +4,12 @@ namespace PhpList\RestBundle\Messaging\Service; +use Doctrine\ORM\EntityManagerInterface; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; use PhpList\Core\Domain\Messaging\Model\Filter\MessageFilter; use PhpList\Core\Domain\Messaging\Model\Message; -use PhpList\Core\Domain\Messaging\Service\MessageManager; +use PhpList\Core\Domain\Messaging\Service\Manager\MessageManager; use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; use PhpList\RestBundle\Messaging\Request\CreateMessageRequest; use PhpList\RestBundle\Messaging\Request\UpdateMessageRequest; @@ -23,6 +24,7 @@ public function __construct( private readonly MessageManager $messageManager, private readonly PaginatedDataProvider $paginatedProvider, private readonly MessageNormalizer $normalizer, + private readonly EntityManagerInterface $entityManager, ) { } @@ -86,5 +88,6 @@ public function deleteMessage(Administrator $administrator, Message $message = n } $this->messageManager->delete($message); + $this->entityManager->flush(); } } diff --git a/src/Subscription/Controller/BlacklistController.php b/src/Subscription/Controller/BlacklistController.php new file mode 100644 index 00000000..9b4e9580 --- /dev/null +++ b/src/Subscription/Controller/BlacklistController.php @@ -0,0 +1,288 @@ +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 + ); + $this->entityManager->flush(); + $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); + $this->entityManager->flush(); + + 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/Controller/SubscribePageController.php b/src/Subscription/Controller/SubscribePageController.php new file mode 100644 index 00000000..ef7a59c3 --- /dev/null +++ b/src/Subscription/Controller/SubscribePageController.php @@ -0,0 +1,439 @@ + '\\d+'], methods: ['GET'])] + #[OA\Get( + path: '/api/v2/subscribe-pages/{id}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Get subscribe page', + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/SubscribePage'), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ), + ] + )] + public function getPage( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to view subscribe pages.'); + } + + if (!$page) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + return $this->json($this->normalizer->normalize($page), Response::HTTP_OK); + } + + #[Route('', name: 'create', methods: ['POST'])] + #[OA\Post( + path: '/api/v2/subscribe-pages', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Create subscribe page', + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'title', type: 'string'), + new OA\Property(property: 'active', type: 'boolean', nullable: true), + ] + ) + ), + tags: ['subscriptions'], + 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: 'Created', + content: new OA\JsonContent(ref: '#/components/schemas/SubscribePage') + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 422, + description: 'Validation failed', + content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse') + ) + ] + )] + public function createPage(Request $request): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to create subscribe pages.'); + } + + /** @var SubscribePageRequest $createRequest */ + $createRequest = $this->validator->validate($request, SubscribePageRequest::class); + + $page = $this->subscribePageManager->createPage($createRequest->title, $createRequest->active, $admin); + $this->entityManager->flush(); + + return $this->json($this->normalizer->normalize($page), Response::HTTP_CREATED); + } + + #[Route('/{id}', name: 'update', requirements: ['id' => '\\d+'], methods: ['PUT'])] + #[OA\Put( + path: '/api/v2/subscribe-pages/{id}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Update subscribe page', + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'title', type: 'string', nullable: true), + new OA\Property(property: 'active', type: 'boolean', nullable: true), + ] + ) + ), + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/SubscribePage') + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ), + ] + )] + public function updatePage( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to update subscribe pages.'); + } + + if (!$page) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + /** @var SubscribePageRequest $updateRequest */ + $updateRequest = $this->validator->validate($request, SubscribePageRequest::class); + + $updated = $this->subscribePageManager->updatePage( + page: $page, + title: $updateRequest->title, + active: $updateRequest->active, + owner: $admin, + ); + $this->entityManager->flush(); + + return $this->json($this->normalizer->normalize($updated), Response::HTTP_OK); + } + + #[Route('/{id}', name: 'delete', requirements: ['id' => '\\d+'], methods: ['DELETE'])] + #[OA\Delete( + path: '/api/v2/subscribe-pages/{id}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Delete subscribe page', + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response(response: 204, description: 'No Content'), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function deletePage( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to delete subscribe pages.'); + } + + if ($page === null) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + $this->subscribePageManager->deletePage($page); + $this->entityManager->flush(); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route('/{id}/data', name: 'get_data', requirements: ['id' => '\\d+'], methods: ['GET'])] + #[OA\Get( + path: '/api/v2/subscribe-pages/{id}/data', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Get subscribe page data', + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items( + properties: [ + new OA\Property(property: 'id', type: 'integer'), + new OA\Property(property: 'name', type: 'string'), + new OA\Property(property: 'data', type: 'string', nullable: true), + ], + type: 'object' + ) + ) + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function getPageData( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to view subscribe page data.'); + } + + if (!$page) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + $data = $this->subscribePageManager->getPageData($page); + + $json = array_map(static function ($item) { + return [ + 'id' => $item->getId(), + 'name' => $item->getName(), + 'data' => $item->getData(), + ]; + }, $data); + + return $this->json($json, Response::HTTP_OK); + } + + #[Route('/{id}/data', name: 'set_data', requirements: ['id' => '\\d+'], methods: ['PUT'])] + #[OA\Put( + path: '/api/v2/subscribe-pages/{id}/data', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Set subscribe page data item', + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'name', type: 'string'), + new OA\Property(property: 'value', type: 'string', nullable: true), + ] + ) + ), + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'id', type: 'integer'), + new OA\Property(property: 'name', type: 'string'), + new OA\Property(property: 'data', type: 'string', nullable: true), + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function setPageData( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to update subscribe page data.'); + } + + if (!$page) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + /** @var SubscribePageDataRequest $createRequest */ + $createRequest = $this->validator->validate($request, SubscribePageDataRequest::class); + + $item = $this->subscribePageManager->setPageData($page, $createRequest->name, $createRequest->value); + $this->entityManager->flush(); + + return $this->json([ + 'id' => $item->getId(), + 'name' => $item->getName(), + 'data' => $item->getData(), + ], Response::HTTP_OK); + } +} diff --git a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php index 6d7bad3a..e096552e 100644 --- a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php +++ b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\Core\Domain\Subscription\Service\Manager\AttributeDefinitionManager; @@ -32,7 +33,8 @@ public function __construct( RequestValidator $validator, AttributeDefinitionManager $definitionManager, AttributeDefinitionNormalizer $normalizer, - PaginatedDataProvider $paginatedDataProvider + PaginatedDataProvider $paginatedDataProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->definitionManager = $definitionManager; @@ -87,6 +89,7 @@ public function create(Request $request): JsonResponse $definitionRequest = $this->validator->validate($request, CreateAttributeDefinitionRequest::class); $attributeDefinition = $this->definitionManager->create($definitionRequest->getDto()); + $this->entityManager->flush(); $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_CREATED); @@ -154,6 +157,7 @@ public function update( attributeDefinition: $attributeDefinition, attributeDefinitionDto: $definitionRequest->getDto(), ); + $this->entityManager->flush(); $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_OK); @@ -209,6 +213,7 @@ public function delete( } $this->definitionManager->delete($attributeDefinition); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Subscription/Controller/SubscriberAttributeValueController.php b/src/Subscription/Controller/SubscriberAttributeValueController.php index 834b0977..24b54209 100644 --- a/src/Subscription/Controller/SubscriberAttributeValueController.php +++ b/src/Subscription/Controller/SubscriberAttributeValueController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberAttributeValueFilter; use PhpList\Core\Domain\Subscription\Model\Subscriber; @@ -33,7 +34,8 @@ public function __construct( RequestValidator $validator, SubscriberAttributeManager $attributeManager, SubscriberAttributeValueNormalizer $normalizer, - PaginatedDataProvider $paginatedDataProvider + PaginatedDataProvider $paginatedDataProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->attributeManager = $attributeManager; @@ -193,6 +195,7 @@ public function delete( throw $this->createNotFoundException('Subscriber attribute not found.'); } $this->attributeManager->delete($attribute); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -349,6 +352,7 @@ public function getAttributeDefinition( } $attribute = $this->attributeManager->getSubscriberAttribute($subscriber->getId(), $definition->getId()); $this->attributeManager->delete($attribute); + $this->entityManager->flush(); return $this->json( $this->normalizer->normalize($attribute), diff --git a/src/Subscription/Controller/SubscriberController.php b/src/Subscription/Controller/SubscriberController.php index fc7be66b..e144e203 100644 --- a/src/Subscription/Controller/SubscriberController.php +++ b/src/Subscription/Controller/SubscriberController.php @@ -4,19 +4,23 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Validator\RequestValidator; use PhpList\RestBundle\Subscription\Request\CreateSubscriberRequest; use PhpList\RestBundle\Subscription\Request\UpdateSubscriberRequest; -use PhpList\RestBundle\Subscription\Service\SubscriberService; +use PhpList\RestBundle\Subscription\Serializer\SubscriberNormalizer; +use PhpList\RestBundle\Subscription\Service\SubscriberHistoryService; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Attribute\Route; /** @@ -28,16 +32,16 @@ #[Route('/subscribers', name: 'subscriber_')] class SubscriberController extends BaseController { - private SubscriberService $subscriberService; - public function __construct( Authentication $authentication, RequestValidator $validator, - SubscriberService $subscriberService, + private readonly SubscriberManager $subscriberManager, + private readonly SubscriberNormalizer $subscriberNormalizer, + private readonly SubscriberHistoryService $subscriberHistoryService, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->authentication = $authentication; - $this->subscriberService = $subscriberService; } #[Route('', name: 'create', methods: ['POST'])] @@ -93,7 +97,9 @@ public function createSubscriber(Request $request): JsonResponse /** @var CreateSubscriberRequest $subscriberRequest */ $subscriberRequest = $this->validator->validate($request, CreateSubscriberRequest::class); - $subscriberData = $this->subscriberService->createSubscriber($subscriberRequest); + $subscriber = $this->subscriberManager->createSubscriber($subscriberRequest->getDto()); + $this->entityManager->flush(); + $subscriberData = $this->subscriberNormalizer->normalize($subscriber, 'json'); return $this->json($subscriberData, Response::HTTP_CREATED); } @@ -163,7 +169,9 @@ public function updateSubscriber( } /** @var UpdateSubscriberRequest $updateSubscriberRequest */ $updateSubscriberRequest = $this->validator->validate($request, UpdateSubscriberRequest::class); - $subscriberData = $this->subscriberService->updateSubscriber($updateSubscriberRequest); + $subscriber = $this->subscriberManager->updateSubscriber($updateSubscriberRequest->getDto(), $admin); + $this->entityManager->flush(); + $subscriberData = $this->subscriberNormalizer->normalize($subscriber, 'json'); return $this->json($subscriberData, Response::HTTP_OK); } @@ -213,7 +221,8 @@ public function getSubscriber(Request $request, int $subscriberId): JsonResponse { $this->requireAuthentication($request); - $subscriberData = $this->subscriberService->getSubscriber($subscriberId); + $subscriber = $this->subscriberManager->getSubscriberById($subscriberId); + $subscriberData = $this->subscriberNormalizer->normalize($subscriber); return $this->json($subscriberData, Response::HTTP_OK); } @@ -309,7 +318,7 @@ public function getSubscriberHistory( ): JsonResponse { $this->requireAuthentication($request); - $historyData = $this->subscriberService->getSubscriberHistory($request, $subscriber); + $historyData = $this->subscriberHistoryService->getSubscriberHistory($request, $subscriber); return $this->json( data: $historyData, @@ -370,11 +379,82 @@ public function deleteSubscriber( if (!$subscriber) { throw $this->createNotFoundException('Subscriber not found.'); } - $this->subscriberService->deleteSubscriber($subscriber); + $this->subscriberManager->deleteSubscriber($subscriber); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } + #[Route( + '/{subscriberId}/reset-bounce-count', + name: 'reset_bounce_count', + requirements: ['subscriberId' => '\d+'], + methods: ['POST'] + )] + #[OA\Post( + path: '/api/v2/subscribers/{subscriberId}/reset-bounce-count', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Reset bounce count for a subscriber.', + tags: ['subscribers'], + 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: 'subscriberId', + description: 'Subscriber ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/Subscriber'), + ), + 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') + ), + new OA\Response( + response: 404, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function resetBounceCount( + Request $request, + #[MapEntity(mapping: ['subscriberId' => 'id'])] ?Subscriber $subscriber = null, + ): Response { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to manage Subscribers.'); + } + + if (!$subscriber) { + throw $this->createNotFoundException('Subscriber not found.'); + } + + $subscriber = $this->subscriberManager->resetBounceCount($subscriber); + $this->entityManager->flush(); + $subscriberData = $this->subscriberNormalizer->normalize($subscriber, 'json'); + + return $this->json($subscriberData, Response::HTTP_OK); + } + #[Route('/confirm', name: 'confirm', methods: ['GET'])] #[OA\Get( path: '/api/v2/subscribers/confirm', @@ -416,9 +496,10 @@ public function setSubscriberAsConfirmed(Request $request): Response return new Response('
Hi [FIRST NAME%%there], remember us? You first signed up for our email newsletter on [ENTERED] – please click here to confirm you're happy to continue receiving our messages:
+ +Continue receiving messages (If you do not confirm using this link, then you won't hear from us again)
+ +While you're at it, you can also update your preferences, including your email address or other details, by clicking here:
+ + + +By confirming your membership and keeping your details up to date, you're helping us to manage and protect your data in accordance with best practices.
+ +Thank you!
","","-- + +This message was sent to [EMAIL] by [FROMEMAIL].
+To forward this message, please do not use the forward button of your email application, because this message was made specifically for you only. Instead use the forward page in our newsletter system.
+ To change your details and to choose which lists to be subscribed to, visit your personal preferences page.
+ Or you can opt-out completely from all future mailings.