Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions .github/workflows/client-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ name: Update phplist-api-client OpenAPI
on:
push:
branches:
- main
- '**'
pull_request:

jobs:
generate-openapi:
runs-on: ubuntu-22.04
outputs:
source_branch: ${{ steps.branch.outputs.source_branch }}
steps:
- name: Determine source branch
id: branch
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "source_branch=${{ github.head_ref }}" >> "$GITHUB_OUTPUT"
else
echo "source_branch=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
fi

- name: Checkout Source Repository
uses: actions/checkout@v3

Expand Down Expand Up @@ -42,6 +53,8 @@ jobs:
update-phplist-api-client:
runs-on: ubuntu-22.04
needs: generate-openapi
env:
TARGET_BRANCH: ${{ needs.generate-openapi.outputs.source_branch }}
steps:
- name: Checkout phplist-api-client Repository
uses: actions/checkout@v3
Expand All @@ -50,6 +63,17 @@ jobs:
token: ${{ secrets.PUSH_API_CLIENT }}
fetch-depth: 0

- name: Prepare target branch
run: |
git fetch origin

if git ls-remote --exit-code --heads origin "$TARGET_BRANCH" >/dev/null 2>&1; then
git checkout "$TARGET_BRANCH"
git pull --rebase origin "$TARGET_BRANCH"
else
git checkout -b "$TARGET_BRANCH"
fi

- name: Download Generated OpenAPI JSON
uses: actions/download-artifact@v4
with:
Expand All @@ -63,24 +87,31 @@ jobs:
if [ -f openapi.json ]; then
diff openapi.json new-openapi/latest-restapi.json > openapi-diff.txt || true
if [ -s openapi-diff.txt ]; then
echo "diff=true" >> $GITHUB_OUTPUT
echo "diff=true" >> "$GITHUB_OUTPUT"
else
echo "diff=false" >> $GITHUB_OUTPUT
echo "diff=false" >> "$GITHUB_OUTPUT"
fi
else
echo "No previous openapi.json, will add."
echo "diff=true" >> $GITHUB_OUTPUT
echo "diff=true" >> "$GITHUB_OUTPUT"
fi

- name: Update and Commit OpenAPI File
if: steps.diff.outputs.diff == 'true'
run: |
set -euo pipefail
cp new-openapi/latest-restapi.json openapi.json
git config user.name "github-actions"
git config user.email "github-actions@phplist-api-client.workflow"
git add openapi.json
git commit -m "Update openapi.json from REST API workflow `date`"
git push
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update openapi.json from REST API workflow $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
git fetch origin "$TARGET_BRANCH"
git rebase "origin/$TARGET_BRANCH"
git push origin HEAD:"$TARGET_BRANCH"

- name: Skip Commit if No Changes
if: steps.diff.outputs.diff == 'false'
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,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",
Expand Down
6 changes: 3 additions & 3 deletions config/services/managers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Identity\Service\SessionManager:
PhpList\Core\Domain\Identity\Service\Manager\SessionManager:
autowire: true
autoconfigure: true

Expand Down Expand Up @@ -36,7 +36,7 @@ services:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Identity\Service\AdministratorManager:
PhpList\Core\Domain\Identity\Service\Manager\AdministratorManager:
autowire: true
autoconfigure: true

Expand All @@ -56,6 +56,6 @@ services:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Identity\Service\PasswordManager:
PhpList\Core\Domain\Identity\Service\Manager\PasswordManager:
autowire: true
autoconfigure: true
7 changes: 6 additions & 1 deletion config/services/messenger_handlers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
services:
PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessorMessageHandler:
PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\CampaignProcessorMessageHandler:
autowire: true
autoconfigure: true
public: false

PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\TestCampaignProcessorMessageHandler:
autowire: true
autoconfigure: true
public: false
6 changes: 6 additions & 0 deletions config/services/normalizers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,11 @@ services:
$classMetadataFactory: '@?serializer.mapping.class_metadata_factory'
$nameConverter: '@Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter'

phplist.request_serializer:
class: Symfony\Component\Serializer\Serializer
arguments:
$normalizers:
- '@Symfony\Component\Serializer\Normalizer\ObjectNormalizer'

PhpList\RestBundle\:
resource: '../../src/*/Serializer/*'
8 changes: 6 additions & 2 deletions config/services/validators.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
services:
PhpList\RestBundle\Common\Validator\RequestValidator:
arguments:
$serializer: '@Symfony\Component\Serializer\Normalizer\ObjectNormalizer'
$serializer: '@phplist.request_serializer'
$validator: '@validator'

PhpList\RestBundle\Identity\Validator\Constraint\UniqueEmailValidator:
Expand All @@ -27,6 +27,11 @@ services:
PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholderValidator:
tags: ['validator.constraint_validator']

PhpList\RestBundle\Messaging\Validator\Constraint\UniqueTemplateTitleValidator:
autowire: true
autoconfigure: true
tags: [ 'validator.constraint_validator' ]

PhpList\RestBundle\Identity\Validator\Constraint\UniqueLoginNameValidator:
autowire: true
autoconfigure: true
Expand All @@ -50,4 +55,3 @@ services:
autowire: true
autoconfigure: true
tags: [ 'validator.constraint_validator' ]

63 changes: 58 additions & 5 deletions src/Common/EventListener/ExceptionListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Exception\ValidatorException;

class ExceptionListener
Expand All @@ -24,7 +26,6 @@ class ExceptionListener
SubscriptionCreationException::class => null,
AttributeDefinitionCreationException::class => null,
AdminAttributeCreationException::class => null,
ValidatorException::class => 400,
AccessDeniedException::class => 403,
AccessDeniedHttpException::class => 403,
AttachmentFileNotFoundException::class => 404,
Expand All @@ -36,33 +37,85 @@ public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();

if ($exception instanceof ValidationFailedException
|| $exception instanceof ValidatorException
|| $exception instanceof UnprocessableEntityHttpException
) {
$event->setResponse(
new JsonResponse([
'message' => 'Validation failed',
'errors' => $this->parseFlatValidationMessage($exception->getMessage()),
], 422)
);

return;
}

foreach (self::EXCEPTION_STATUS_MAP as $class => $statusCode) {
if ($exception instanceof $class) {
$status = $statusCode ?? $exception->getStatusCode();
$status = $statusCode ?? (
method_exists($exception, 'getStatusCode')
? $exception->getStatusCode()
: 400
);

$event->setResponse(
new JsonResponse([
'message' => $exception->getMessage()
'message' => $exception->getMessage(),
], $status)
);

return;
}
}

if ($exception instanceof HttpExceptionInterface) {
$event->setResponse(
new JsonResponse([
'message' => $exception->getMessage()
'message' => $exception->getMessage(),
], $exception->getStatusCode())
);

return;
}

if ($exception instanceof Exception) {
$event->setResponse(
new JsonResponse([
'message' => $exception->getMessage()
'message' => $exception->getMessage(),
], 500)
);
}
}

/**
* @return array<string, array<int, string>>
*/
private function parseFlatValidationMessage(string $message): array
{
$errors = [];
$lines = preg_split('/\r\n|\r|\n/', $message) ?: [];

foreach ($lines as $line) {
$line = trim($line);

if ($line === '') {
continue;
}

$parts = explode(':', $line, 2);

if (count($parts) !== 2) {
$errors['_global'][] = $line;
continue;
}

$field = trim($parts[0]);
$errorMessage = trim($parts[1]);

$errors[$field][] = $errorMessage;
}

return $errors;
}
}
36 changes: 34 additions & 2 deletions src/Common/SwaggerSchemasResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,22 @@

use OpenApi\Attributes as OA;

#[OA\Schema(
schema: 'ErrorDetails',
type: 'object',
example: [
'format.formatOptions[0]' => ['The value you selected is not a valid choice.'],
'schedule.repeatUntil' => ['This value is not a valid datetime.'],
'schedule.requeueUntil' => ['This value is not a valid datetime.'],
],
additionalProperties: new OA\AdditionalProperties(
type: 'array',
items: new OA\Items(type: 'string')
)
)]
#[OA\Schema(
schema: 'UnauthorizedResponse',
required: ['message'],
properties: [
new OA\Property(
property: 'message',
Expand All @@ -19,17 +33,23 @@
)]
#[OA\Schema(
schema: 'ValidationErrorResponse',
required: ['message', 'errors'],
properties: [
new OA\Property(
property: 'message',
type: 'string',
example: 'Some fields are invalid'
example: 'Validation failed'
),
new OA\Property(
property: 'errors',
ref: '#/components/schemas/ErrorDetails'
)
],
type: 'object'
)]
#[OA\Schema(
schema: 'BadRequestResponse',
required: ['message'],
properties: [
new OA\Property(
property: 'message',
Expand All @@ -41,6 +61,7 @@
)]
#[OA\Schema(
schema: 'AlreadyExistsResponse',
required: ['message'],
properties: [
new OA\Property(
property: 'message',
Expand All @@ -62,7 +83,18 @@
],
type: 'object'
)]

#[OA\Schema(
schema: 'GenericErrorResponse',
required: ['message'],
properties: [
new OA\Property(
property: 'message',
type: 'string',
example: 'An unexpected error occurred.'
)
],
type: 'object'
)]
#[OA\Schema(
schema: 'CursorPagination',
properties: [
Expand Down
3 changes: 3 additions & 0 deletions src/Common/Validator/RequestValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public function validate(Request $request, string $dtoClass): RequestInterface
if (isset($routeParams['listId'])) {
$routeParams['listId'] = (int) $routeParams['listId'];
}
if (isset($routeParams['templateId'])) {
$routeParams['templateId'] = (int) $routeParams['templateId'];
}

$data = array_merge($routeParams, $request->query->all(), $body ?? []);

Expand Down
3 changes: 2 additions & 1 deletion src/Identity/Serializer/AdministratorTokenNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PhpList\RestBundle\Identity\Serializer;

use DateTimeInterface;
use PhpList\Core\Domain\Identity\Model\AdministratorToken;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

Expand All @@ -21,7 +22,7 @@ public function normalize($object, string $format = null, array $context = []):
return [
'id' => $object->getId(),
'key' => $object->getKey(),
'expiry_date' => $object->getExpiry()->format('Y-m-d\TH:i:sP'),
'expiry_date' => $object->getExpiry()->format(DateTimeInterface::ATOM),
];
}

Expand Down
Loading
Loading