Skip to content

Commit

Permalink
[TASK] Use form to validate API endpoint for site package generation (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminkott authored Nov 28, 2024
1 parent 38e981b commit 81813f6
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 60 deletions.
6 changes: 0 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,6 @@ parameters:
count: 2
path: src/Command/ListMissingDownloadsCommand.php

-
message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(mixed\)\: mixed\)\|null, Closure\(Symfony\\Component\\Validator\\ConstraintViolationInterface\)\: \(string\|Stringable\) given\.$#'
identifier: argument.type
count: 1
path: src/Controller/Api/SitepackageController.php

-
message: '#^Parameter \#1 \$packages of method App\\Service\\ComposerPackagesService\:\:cleanPackagesForVersions\(\) expects array\<string, bool\|string\>, array\<mixed\> given\.$#'
identifier: argument.type
Expand Down
48 changes: 48 additions & 0 deletions src/Controller/Api/AbstractController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@
use App\Repository\ReleaseRepository;
use App\Repository\RequirementRepository;
use App\Service\CacheService;
use App\Service\SitepackageGenerator;
use App\Utility\VersionUtility;
use Doctrine\Inflector\InflectorFactory;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormErrorIterator;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Serializer\SerializerInterface;
Expand All @@ -51,6 +57,7 @@ public function __construct(
private RequirementRepository $requirements,
private ReleaseRepository $releases,
private ValidatorInterface $validator,
private SitepackageGenerator $sitepackageGenerator,
) {}

protected function getCache(): TagAwareCacheInterface
Expand Down Expand Up @@ -88,6 +95,11 @@ protected function getReleases(): ReleaseRepository
return $this->releases;
}

protected function getSitepackageGenerator(): SitepackageGenerator
{
return $this->sitepackageGenerator;
}

protected function findMajorVersion(string $version): MajorVersion
{
$this->checkMajorVersionFormat($version);
Expand Down Expand Up @@ -214,4 +226,40 @@ protected function flat(array $array, string $prefix = ''): array

return $result;
}

/**
* @template T
*
* @param FormInterface<T> $form
*/
protected function sendErroneousResponse(FormInterface $form): Response
{
return new JsonResponse([
'errors' => $this->getErrors($form),
], Response::HTTP_BAD_REQUEST);
}

/**
* @template T
*
* @param FormInterface<T> $form
*
* @return array<int|string, mixed>
*/
private function getErrors(FormInterface $form): array
{
$errors = [];
/** @var FormErrorIterator<FormError> $formErrors */
$formErrors = $form->getErrors();
foreach ($formErrors as $error) {
$errors[] = $error->getMessage();
}
foreach ($form->all() as $childForm) {
if (($childForm instanceof FormInterface) && count($childErrors = $this->getErrors($childForm)) > 0) {
$errors[$childForm->getName()] = $childErrors;
}
}

return $errors;
}
}
64 changes: 27 additions & 37 deletions src/Controller/Api/SitepackageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,64 +24,54 @@
namespace App\Controller\Api;

use App\Entity\Sitepackage;
use App\Service\SitepackageGenerator;
use App\Form\SitepackageType;
use App\Utility\StringUtility;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Validation;

#[Route(path: '/api/v1/sitepackage', defaults: ['_format' => 'json'])]
class SitepackageController extends AbstractController
{
public function __construct(
protected SerializerInterface $serializer,
protected SitepackageGenerator $sitepackageGenerator
) {}

#[Route(path: '/', methods: ['POST'])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: new Model(type: Sitepackage::class)))]
#[OA\RequestBody(
required: true,
content: new OA\JsonContent(ref: new Model(type: SitepackageType::class, options: ['csrf_protection' => false])),
)]
#[OA\Response(response: 200, description: 'Successfully generated.', content: new OA\MediaType(mediaType: 'application/zip'))]
#[OA\Response(response: 400, description: 'Request malformed.')]
#[OA\Tag(name: 'sitepackage')]
public function createSitepackage(Request $request): Response
{
$content = $request->getContent();
/** @var Sitepackage $sitepackage */
$sitepackage = $this->serializer->deserialize($content, Sitepackage::class, 'json');
$this->validateObject($sitepackage);
$content = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($content)) {
$content = [];
}

$sitepackage->setVendorName(StringUtility::stringToUpperCamelCase($sitepackage->getAuthor()->getCompany()));
$sitepackage->setVendorNameAlternative(StringUtility::camelCaseToLowerCaseDashed($sitepackage->getVendorName()));
$sitepackage->setPackageName(StringUtility::stringToUpperCamelCase($sitepackage->getTitle()));
$sitepackage->setPackageNameAlternative(StringUtility::camelCaseToLowerCaseDashed($sitepackage->getPackageName()));
$sitepackage->setExtensionKey(StringUtility::camelCaseToLowerCaseUnderscored($sitepackage->getPackageName()));
$sitepackage = new Sitepackage();
$form = $this->createForm(SitepackageType::class, $sitepackage, ['csrf_protection' => false]);
$form->submit($content, true);

$this->sitepackageGenerator->create($sitepackage);
$filename = $this->sitepackageGenerator->getFilename();
BinaryFileResponse::trustXSendfileTypeHeader();
if ($form->isValid()) {
$sitepackage->setVendorName(StringUtility::stringToUpperCamelCase($sitepackage->getAuthor()->getCompany()));
$sitepackage->setVendorNameAlternative(StringUtility::camelCaseToLowerCaseDashed($sitepackage->getVendorName()));
$sitepackage->setPackageName(StringUtility::stringToUpperCamelCase($sitepackage->getTitle()));
$sitepackage->setPackageNameAlternative(StringUtility::camelCaseToLowerCaseDashed($sitepackage->getPackageName()));
$sitepackage->setExtensionKey(StringUtility::camelCaseToLowerCaseUnderscored($sitepackage->getPackageName()));

return $this
->file($this->sitepackageGenerator->getZipPath(), StringUtility::toASCII($filename))
->deleteFileAfterSend(true);
}
$sitepackageGenerator = $this->getSitepackageGenerator();
$sitepackageGenerator->create($sitepackage);
$filename = $sitepackageGenerator->getFilename();
BinaryFileResponse::trustXSendfileTypeHeader();

protected function validateObject(mixed $object): void
{
$validator = Validation::createValidatorBuilder()
->enableAttributeMapping()
->getValidator();
$errors = $validator->validate($object);
if (\count($errors) > 0) {
$errorsString = implode("\n", array_map(static fn(ConstraintViolationInterface $x) => $x->getMessage(), (array)$errors));
throw new BadRequestHttpException($errorsString);
return $this
->file($sitepackageGenerator->getZipPath(), StringUtility::toASCII($filename))
->deleteFileAfterSend(true);
}

return $this->sendErroneousResponse($form);
}
}
6 changes: 0 additions & 6 deletions src/Entity/Sitepackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
namespace App\Entity;

use App\Entity\Sitepackage\Author;
use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

/**
Expand All @@ -34,12 +33,10 @@ class Sitepackage implements \JsonSerializable
{
#[Assert\NotBlank]
#[Assert\Choice(['bootstrap_package', 'fluid_styled_content'])]
#[OA\Property(type: 'string', example: 'bootstrap_package')]
private string $basePackage = 'bootstrap_package';

#[Assert\NotBlank]
#[Assert\Choice([10.4, 11.5, 12.4, 13.4])]
#[OA\Property(type: 'float', example: 13.4)]
private float $typo3Version = 13.4;

private string $vendorName;
Expand All @@ -49,19 +46,16 @@ class Sitepackage implements \JsonSerializable
#[Assert\NotBlank(message: 'Please enter a title for your site package')]
#[Assert\Length(min: 3)]
#[Assert\Regex(pattern: '/^[A-Za-z0-9\x7f-\xff .:&-]+$/', message: 'Only letters, numbers and spaces are allowed')]
#[OA\Property(type: 'string', example: 'My Sitepackage')]
private string $title;

#[Assert\Regex(pattern: '/^[A-Za-z0-9\x7f-\xff .,:!?&-]+$/', message: 'Only letters, numbers and spaces are allowed')]
#[OA\Property(type: 'string', example: 'Project Configuration for Client')]
private string $description;

private string $packageName;
private string $packageNameAlternative;
private string $extensionKey;

#[Assert\Url]
#[OA\Property(type: 'string', example: 'https://github.com/FriendsOfTYPO3/introduction')]
private string $repositoryUrl = '';

#[Assert\Valid]
Expand Down
5 changes: 0 additions & 5 deletions src/Entity/Sitepackage/Author.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

namespace App\Entity\Sitepackage;

use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

/**
Expand All @@ -33,23 +32,19 @@ class Author implements \JsonSerializable
{
#[Assert\NotBlank(message: "Please enter the authors' name.")]
#[Assert\Length(min: 3)]
#[OA\Property(type: 'string', example: 'J. Doe')]
private string $name;

#[Assert\NotBlank(message: "Please enter the authors' email address.")]
#[Assert\Email(message: "The email '{{ value }}' is not a valid email.")]
#[OA\Property(type: 'string', example: '[email protected]')]
private string $email;

#[Assert\NotBlank(message: "Please enter the authors' company.")]
#[Assert\Length(min: 3)]
#[Assert\Regex(pattern: '/^[A-Za-z0-9\x7f-\xff .:&-]+$/', message: 'Only letters, numbers and spaces are allowed')]
#[OA\Property(type: 'string', example: 'TYPO3')]
private string $company;

#[Assert\NotBlank(message: "Please enter the authors' homepage URL.")]
#[Assert\Url]
#[OA\Property(type: 'string', example: 'https://typo3.com')]
private string $homepage;

public function getName(): string
Expand Down
12 changes: 12 additions & 0 deletions src/Form/AuthorType.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,36 @@ public function buildForm(FormBuilderInterface $builder, array $options)
'autocomplete' => 'off',
'placeholder' => 'John Doe',
],
'documentation' => [
'example' => 'J. Doe',
],
])
->add('email', EmailType::class, [
'attr' => [
'autocomplete' => 'off',
'placeholder' => '[email protected]',
],
'documentation' => [
'example' => '[email protected]',
],
])
->add('company', TextType::class, [
'attr' => [
'autocomplete' => 'off',
'placeholder' => 'Company Inc.',
],
'documentation' => [
'example' => 'TYPO3',
],
])
->add('homepage', TextType::class, [
'attr' => [
'autocomplete' => 'off',
'placeholder' => 'https://www.example.com',
],
'documentation' => [
'example' => 'https://typo3.com',
],
]);
}

Expand Down
19 changes: 15 additions & 4 deletions src/Form/SitepackageType.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->setAction($options['action'])
->add('basePackage', ChoiceType::class, [
->add('base_package', ChoiceType::class, [
'label' => 'Base Package',
'choices' => [
'Bootstrap Package' => 'bootstrap_package',
'Fluid Styled Content' => 'fluid_styled_content',
],
'expanded' => true,
])
->add('typo3Version', ChoiceType::class, [
->add('typo3_version', ChoiceType::class, [
'label' => 'TYPO3 Version',
'choices' => [
'13.4' => 13.4,
Expand All @@ -63,6 +63,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'autocomplete' => 'off',
'placeholder' => 'My Site Package',
],
'documentation' => [
'example' => 'My Sitepackage',
],
])
->add('description', TextareaType::class, [
'required' => false,
Expand All @@ -71,17 +74,25 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'autocomplete' => 'off',
'placeholder' => 'Optional description for the use of this Site Package',
],
'documentation' => [
'example' => 'Project Configuration for Client',
],
])
->add('repositoryUrl', TextType::class, [
->add('repository_url', TextType::class, [
'label' => 'Repository URL',
'required' => false,
'empty_data' => '',
'attr' => [
'autocomplete' => 'off',
'placeholder' => 'https://github.com/username/my_sitepackage',
],
'documentation' => [
'example' => 'https://github.com/FriendsOfTYPO3/introduction',
],
])
->add('author', AuthorType::class);
->add('author', AuthorType::class, [
'csrf_protection' => false,
]);
}

public function setDefaultOptions(OptionsResolver $resolver): void
Expand Down
3 changes: 1 addition & 2 deletions src/Service/SitepackageGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ public function create(Sitepackage $package): void
$fileList = FileUtility::listDirectory($sourceDir);

$zipFile = new \ZipArchive();
$opened = $zipFile->open($this->zipPath, \ZipArchive::CREATE);
if ($opened === true) {
if ($zipFile->open($this->zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true) {
foreach ($fileList as $file) {
if ($file !== $this->zipPath && file_exists($file)) {
$baseFileName = $this->createRelativeFilePath($file, $sourceDir);
Expand Down
Loading

0 comments on commit 81813f6

Please sign in to comment.