Repository navigation
Dev #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Dev #173
Changes from all commits
98bee7d
0818714
fb0b5f2
35f6bc5
df43ce5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace PhpList\RestBundle\Messaging\Controller; | ||
|
|
||
| use Doctrine\ORM\EntityManagerInterface; | ||
| use OpenApi\Attributes as OA; | ||
| use PhpList\Core\Domain\Identity\Service\Authentication; | ||
| use PhpList\Core\Domain\Identity\Service\PermissionChecker; | ||
| use PhpList\Core\Domain\Messaging\Model\Message; | ||
| use PhpList\Core\Domain\Messaging\Service\Manager\MessageDataManager; | ||
| use PhpList\Core\Domain\Messaging\Service\MessageDataLoader; | ||
| use PhpList\RestBundle\Common\Controller\BaseController; | ||
| use PhpList\RestBundle\Common\Validator\RequestValidator; | ||
| use PhpList\RestBundle\Messaging\Dto\CampaignMessageData; | ||
| use PhpList\RestBundle\Messaging\Request\UpdateMessageDataRequest; | ||
| use PhpList\RestBundle\Messaging\Serializer\MessageDataNormalizer; | ||
| use Symfony\Bridge\Doctrine\Attribute\MapEntity; | ||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||
| use Symfony\Component\HttpFoundation\Request; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
| use Symfony\Component\Routing\Attribute\Route; | ||
|
|
||
| /** | ||
| * This controller provides REST API to set the campaign settings that are kept in the message data. | ||
| */ | ||
| #[Route('/campaigns', name: 'campaign_message_data_')] | ||
| class CampaignMessageDataController extends BaseController | ||
| { | ||
| public function __construct( | ||
| Authentication $authentication, | ||
| RequestValidator $validator, | ||
| private readonly MessageDataManager $messageDataManager, | ||
| private readonly MessageDataLoader $messageDataLoader, | ||
| private readonly PermissionChecker $permissionChecker, | ||
| private readonly EntityManagerInterface $entityManager, | ||
| private readonly MessageDataNormalizer $messageDataNormalizer, | ||
| ) { | ||
| parent::__construct($authentication, $validator); | ||
| } | ||
|
|
||
| #[Route( | ||
| '/{messageId}/message-data', | ||
| name: 'update', | ||
| requirements: ['messageId' => '\d+'], | ||
| methods: ['PATCH'] | ||
| )] | ||
| #[OA\Patch( | ||
| path: '/api/v2/campaigns/{messageId}/message-data', | ||
| description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . | ||
| 'Sets campaign settings that are not part of the campaign itself: analytics tracking, notification ' . | ||
| 'addresses and excluded lists. Only the fields that are sent are changed.', | ||
| summary: 'Set campaign message data.', | ||
| requestBody: new OA\RequestBody( | ||
| description: 'The settings to change.', | ||
| required: true, | ||
| content: new OA\JsonContent(ref: '#/components/schemas/UpdateMessageDataRequest') | ||
| ), | ||
| tags: ['campaigns'], | ||
| 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: 'messageId', | ||
| description: 'message ID', | ||
| in: 'path', | ||
| required: true, | ||
| schema: new OA\Schema(type: 'string') | ||
| ) | ||
| ], | ||
| responses: [ | ||
| new OA\Response( | ||
| response: 200, | ||
| description: 'Success, returns all the settings of the campaign', | ||
| content: new OA\JsonContent(ref: '#/components/schemas/MessageData') | ||
| ), | ||
| new OA\Response( | ||
| response: 401, | ||
| description: 'Not authenticated', | ||
| content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') | ||
| ), | ||
| 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') | ||
| ), | ||
| new OA\Response( | ||
| response: 422, | ||
| description: 'Failure', | ||
| content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse') | ||
| ), | ||
| ] | ||
| )] | ||
| public function updateMessageData( | ||
| Request $request, | ||
| #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null, | ||
| ): JsonResponse { | ||
| $authUser = $this->requireAuthentication($request); | ||
| if ($message === null) { | ||
| throw $this->createNotFoundException('Campaign not found.'); | ||
| } | ||
|
|
||
| if (!$this->permissionChecker->canEdit($authUser, $message)) { | ||
| throw $this->createAccessDeniedException('You are not allowed to update campaigns.'); | ||
| } | ||
|
|
||
| /** @var UpdateMessageDataRequest $messageDataRequest */ | ||
| $messageDataRequest = $this->validator->validate($request, UpdateMessageDataRequest::class); | ||
|
|
||
| $this->messageDataManager->storeMessageData($message, $messageDataRequest->getDto()); | ||
| $this->entityManager->flush(); | ||
|
|
||
| $messageData = new CampaignMessageData(($this->messageDataLoader)($message)); | ||
|
|
||
| return $this->json($this->messageDataNormalizer->normalize($messageData), Response::HTTP_OK); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace PhpList\RestBundle\Messaging\Dto; | ||
|
|
||
| /** | ||
| * The message data of a campaign, as loaded by core's MessageDataLoader. | ||
| */ | ||
| class CampaignMessageData | ||
| { | ||
| /** @param array<string, mixed> $values */ | ||
| public function __construct(public readonly array $values) | ||
| { | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace PhpList\RestBundle\Messaging\Request; | ||
|
|
||
| use OpenApi\Attributes as OA; | ||
| use PhpList\RestBundle\Common\Request\RequestInterface; | ||
| use PhpList\RestBundle\Subscription\Validator\Constraint\ListExists; | ||
| use Symfony\Component\Validator\Constraints as Assert; | ||
| use Symfony\Component\Validator\Context\ExecutionContextInterface; | ||
|
|
||
| /** | ||
| * Campaign settings that are kept in the message data, apart from the campaign itself. Only the fields that are | ||
| * sent are changed; an empty string clears a text setting. | ||
| */ | ||
| #[OA\Schema( | ||
| schema: 'UpdateMessageDataRequest', | ||
| properties: [ | ||
| new OA\Property( | ||
| property: 'google_track', | ||
| description: 'Add analytics tracking parameters to the links of the campaign.', | ||
| type: 'boolean', | ||
| example: true | ||
| ), | ||
| new OA\Property(property: 'utm_source', description: 'Google Analytics source.', type: 'string'), | ||
| new OA\Property(property: 'utm_medium', description: 'Google Analytics medium.', type: 'string'), | ||
| new OA\Property(property: 'utm_campaign', description: 'Google Analytics campaign.', type: 'string'), | ||
| new OA\Property(property: 'pk_source', description: 'Matomo source.', type: 'string'), | ||
| new OA\Property(property: 'pk_medium', description: 'Matomo medium.', type: 'string'), | ||
| new OA\Property(property: 'pk_campaign', description: 'Matomo campaign.', type: 'string'), | ||
| new OA\Property( | ||
| property: 'notify_start', | ||
| description: 'Email addresses, separated by a comma, to alert when sending starts.', | ||
| type: 'string', | ||
| example: 'admin@example.com' | ||
| ), | ||
| new OA\Property( | ||
| property: 'notify_end', | ||
| description: 'Email addresses, separated by a comma, to alert when sending finishes.', | ||
| type: 'string', | ||
| example: 'admin@example.com' | ||
| ), | ||
| new OA\Property( | ||
| property: 'exclude_lists', | ||
| description: 'IDs of the lists whose subscribers are left out of the campaign.', | ||
| type: 'array', | ||
| items: new OA\Items(type: 'integer', minimum: 1), | ||
| example: [4, 5] | ||
| ), | ||
| ], | ||
| type: 'object' | ||
| )] | ||
| class UpdateMessageDataRequest implements RequestInterface | ||
| { | ||
| #[Assert\Type('bool')] | ||
| public ?bool $googleTrack = null; | ||
|
|
||
| #[Assert\Length(max: 255)] | ||
| public ?string $utmSource = null; | ||
|
|
||
| #[Assert\Length(max: 255)] | ||
| public ?string $utmMedium = null; | ||
|
|
||
| #[Assert\Length(max: 255)] | ||
| public ?string $utmCampaign = null; | ||
|
|
||
| #[Assert\Length(max: 255)] | ||
| public ?string $pkSource = null; | ||
|
|
||
| #[Assert\Length(max: 255)] | ||
| public ?string $pkMedium = null; | ||
|
|
||
| #[Assert\Length(max: 255)] | ||
| public ?string $pkCampaign = null; | ||
|
|
||
| #[Assert\Length(max: 255)] | ||
| public ?string $notifyStart = null; | ||
|
|
||
| #[Assert\Length(max: 255)] | ||
| public ?string $notifyEnd = null; | ||
|
|
||
| #[Assert\Type('array')] | ||
| #[Assert\All([ | ||
| new Assert\Sequentially([ | ||
| new Assert\Type('integer'), | ||
| new Assert\Positive(), | ||
| new ListExists(), | ||
| ]), | ||
| ])] | ||
| public ?array $excludeLists = null; | ||
|
|
||
| #[Assert\Callback] | ||
| public function validateNotifyAddresses(ExecutionContextInterface $context): void | ||
| { | ||
| foreach (['notifyStart' => $this->notifyStart, 'notifyEnd' => $this->notifyEnd] as $property => $value) { | ||
| if ($value === null || $value === '') { | ||
| continue; | ||
| } | ||
|
|
||
| foreach (array_map('trim', explode(',', $value)) as $address) { | ||
| $context->getValidator() | ||
| ->inContext($context) | ||
| ->atPath($property) | ||
| ->validate($address, [new Assert\NotBlank(), new Assert\Email()]); | ||
| } | ||
| } | ||
|
Comment on lines
+102
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| /** | ||
| * The settings that were sent, by their message data name. | ||
| * | ||
| * @return array<string, bool|string|array<int, int>> | ||
| */ | ||
| public function getDto(): array | ||
| { | ||
| return array_filter([ | ||
| 'google_track' => $this->googleTrack, | ||
| 'utm_source' => $this->utmSource, | ||
| 'utm_medium' => $this->utmMedium, | ||
| 'utm_campaign' => $this->utmCampaign, | ||
| 'pk_source' => $this->pkSource, | ||
| 'pk_medium' => $this->pkMedium, | ||
| 'pk_campaign' => $this->pkCampaign, | ||
| 'notify_start' => $this->notifyStart, | ||
| 'notify_end' => $this->notifyEnd, | ||
| // like phplist3, keyed by list id | ||
| 'excludelist' => $this->excludeLists === null | ||
| ? null | ||
| : array_combine($this->excludeLists, $this->excludeLists), | ||
| ], static fn (mixed $value): bool => $value !== null); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: phpList/rest-api
Length of output: 41488
Script executed:
Repository: phpList/rest-api
Length of output: 23312
Script executed:
Repository: phpList/rest-api
Length of output: 41152
Script executed:
Repository: phpList/rest-api
Length of output: 8513
Document the 401 response on the PATCH operation.
An unauthenticated PATCH returns 401, but the OpenAPI
responseslist omits it. Add a 401 entry so the published contract covers this status.Suggested fix
Committable suggestion