First testable and dev ready version
7 files modified
23 files added
| | |
| | | |
| | | ###> symfony/framework-bundle ### |
| | | APP_ENV=dev |
| | | APP_DEBUG=1 |
| | | APP_SECRET=5f741ee8c025b998f20f53e4f9040374 |
| | | ###< symfony/framework-bundle ### |
| | | |
| | | ###> doctrine/doctrine-bundle ### |
| | | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url |
| | | # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml |
| | | # |
| | | DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" |
| | | # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" |
| | | # DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8" |
| | | ###< doctrine/doctrine-bundle ### |
| | | |
| | | ###> nelmio/cors-bundle ### |
| | | CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' |
| | | ###< nelmio/cors-bundle ### |
| | |
| | | /var/ |
| | | /vendor/ |
| | | ###< symfony/framework-bundle ### |
| | | |
| | | ###> phpunit/phpunit ### |
| | | .phpunit.result.cache |
| | | ###< phpunit/phpunit ### |
| | |
| | | "php": ">=7.2.5", |
| | | "ext-ctype": "*", |
| | | "ext-iconv": "*", |
| | | "api-platform/core": "^2.6", |
| | | "composer/package-versions-deprecated": "1.11.99.4", |
| | | "doctrine/annotations": "^1.0", |
| | | "doctrine/doctrine-bundle": "^2.4", |
| | | "doctrine/doctrine-migrations-bundle": "^3.1", |
| | | "doctrine/orm": "^2.10", |
| | | "nelmio/cors-bundle": "^2.1", |
| | | "phpdocumentor/reflection-docblock": "^5.2", |
| | | "symfony/asset": "5.3.*", |
| | | "symfony/browser-kit": "5.3.*", |
| | | "symfony/console": "5.3.*", |
| | | "symfony/dotenv": "5.3.*", |
| | | "symfony/expression-language": "5.3.*", |
| | | "symfony/flex": "^1.3.1", |
| | | "symfony/framework-bundle": "5.3.*", |
| | | "symfony/http-client": "5.3.*", |
| | | "symfony/property-access": "5.3.*", |
| | | "symfony/property-info": "5.3.*", |
| | | "symfony/proxy-manager-bridge": "5.3.*", |
| | | "symfony/runtime": "5.3.*", |
| | | "symfony/security-bundle": "5.3.*", |
| | | "symfony/serializer": "5.3.*", |
| | | "symfony/twig-bundle": "5.3.*", |
| | | "symfony/validator": "5.3.*", |
| | | "symfony/yaml": "5.3.*" |
| | | }, |
| | | "require-dev": { |
| | | "justinrainbow/json-schema": "^5.2", |
| | | "phpunit/phpunit": "^9.5", |
| | | "symfony/css-selector": "5.3.*", |
| | | "symfony/maker-bundle": "^1.34", |
| | | "symfony/phpunit-bridge": "^5.3" |
| | | }, |
| | | "config": { |
| | | "optimize-autoloader": true, |
| | |
| | | |
| | | return [ |
| | | Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], |
| | | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], |
| | | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], |
| | | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], |
| | | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], |
| | | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], |
| | | ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], |
| | | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], |
| | | ]; |
New file |
| | |
| | | api_platform: |
| | | mapping: |
| | | paths: ['%kernel.project_dir%/src/Entity'] |
| | | patch_formats: |
| | | json: ['application/merge-patch+json'] |
| | | swagger: |
| | | versions: [3] |
| | | |
| | | exception_to_status: |
| | | # this should be left alone, but the original swagger asked for this |
| | | ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException: 405 |
| | | formats: |
| | | jsonld: ['application/ld+json'] |
| | | jsonhal: ['application/hal+json'] |
| | | jsonapi: ['application/vnd.api+json'] |
| | | json: ['application/json'] |
| | | xml: ['application/xml', 'text/xml'] |
| | | yaml: ['application/x-yaml'] |
| | | csv: ['text/csv'] |
| | | html: ['text/html'] |
New file |
| | |
| | | doctrine: |
| | | dbal: |
| | | url: '%env(resolve:DATABASE_URL)%' |
| | | |
| | | # IMPORTANT: You MUST configure your server version, |
| | | # either here or in the DATABASE_URL env var (see .env file) |
| | | #server_version: '13' |
| | | orm: |
| | | auto_generate_proxy_classes: true |
| | | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware |
| | | auto_mapping: true |
| | | mappings: |
| | | App: |
| | | is_bundle: false |
| | | type: annotation |
| | | dir: '%kernel.project_dir%/src/Entity' |
| | | prefix: 'App\Entity' |
| | | alias: App |
New file |
| | |
| | | doctrine_migrations: |
| | | migrations_paths: |
| | | # namespace is arbitrary but should be different from App\Migrations |
| | | # as migrations classes should NOT be autoloaded |
| | | 'DoctrineMigrations': '%kernel.project_dir%/migrations' |
| | | enable_profiler: '%kernel.debug%' |
New file |
| | |
| | | nelmio_cors: |
| | | defaults: |
| | | origin_regex: true |
| | | allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] |
| | | allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] |
| | | allow_headers: ['Content-Type', 'Authorization'] |
| | | expose_headers: ['Link'] |
| | | max_age: 3600 |
| | | paths: |
| | | '^/': null |
New file |
| | |
| | | doctrine: |
| | | orm: |
| | | auto_generate_proxy_classes: false |
| | | query_cache_driver: |
| | | type: pool |
| | | pool: doctrine.system_cache_pool |
| | | result_cache_driver: |
| | | type: pool |
| | | pool: doctrine.result_cache_pool |
| | | |
| | | framework: |
| | | cache: |
| | | pools: |
| | | doctrine.result_cache_pool: |
| | | adapter: cache.app |
| | | doctrine.system_cache_pool: |
| | | adapter: cache.system |
New file |
| | |
| | | security: |
| | | # https://symfony.com/doc/current/security/authenticator_manager.html |
| | | enable_authenticator_manager: true |
| | | # https://symfony.com/doc/current/security.html#c-hashing-passwords |
| | | password_hashers: |
| | | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' |
| | | # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers |
| | | providers: |
| | | users_in_memory: { memory: null } |
| | | firewalls: |
| | | dev: |
| | | pattern: ^/(_(profiler|wdt)|css|images|js)/ |
| | | security: false |
| | | main: |
| | | lazy: true |
| | | provider: users_in_memory |
| | | |
| | | # activate different ways to authenticate |
| | | # https://symfony.com/doc/current/security.html#firewalls-authentication |
| | | |
| | | # https://symfony.com/doc/current/security/impersonating_user.html |
| | | # switch_user: true |
| | | |
| | | # Easy way to control access for large sections of your site |
| | | # Note: Only the *first* access control that matches will be used |
| | | access_control: |
| | | # - { path: ^/admin, roles: ROLE_ADMIN } |
| | | # - { path: ^/profile, roles: ROLE_USER } |
New file |
| | |
| | | doctrine: |
| | | dbal: |
| | | # "TEST_TOKEN" is typically set by ParaTest |
| | | dbname_suffix: '_test%env(default::TEST_TOKEN)%' |
New file |
| | |
| | | framework: |
| | | validation: |
| | | not_compromised_password: false |
New file |
| | |
| | | twig: |
| | | default_path: '%kernel.project_dir%/templates' |
| | | |
| | | when@test: |
| | | twig: |
| | | strict_variables: true |
New file |
| | |
| | | framework: |
| | | validation: |
| | | email_validation_mode: html5 |
| | | |
| | | # Enables validator auto-mapping support. |
| | | # For instance, basic validation constraints will be inferred from Doctrine's metadata. |
| | | #auto_mapping: |
| | | # App\Entity\: [] |
New file |
| | |
| | | controllers: |
| | | resource: ../../src/Controller/ |
| | | type: annotation |
| | | |
| | | kernel: |
| | | resource: ../../src/Kernel.php |
| | | type: annotation |
New file |
| | |
| | | api_platform: |
| | | resource: . |
| | | type: api_platform |
| | | prefix: /v1 |
| | |
| | | parameters: |
| | | |
| | | services: |
| | | # default configuration for services in *this* file |
| | | _defaults: |
| | | autowire: true # Automatically injects dependencies in your services. |
| | | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. |
| | | # default configuration for services in *this* file |
| | | _defaults: |
| | | autowire: true # Automatically injects dependencies in your services. |
| | | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. |
| | | |
| | | # makes classes in src/ available to be used as services |
| | | # this creates a service per class whose id is the fully-qualified class name |
| | | App\: |
| | | resource: '../src/' |
| | | exclude: |
| | | - '../src/DependencyInjection/' |
| | | - '../src/Entity/' |
| | | - '../src/Kernel.php' |
| | | - '../src/Tests/' |
| | | # makes classes in src/ available to be used as services |
| | | # this creates a service per class whose id is the fully-qualified class name |
| | | App\: |
| | | resource: '../src/' |
| | | exclude: |
| | | - '../src/DependencyInjection/' |
| | | - '../src/Entity/' |
| | | - '../src/Kernel.php' |
| | | - '../src/Tests/' |
| | | |
| | | # add more service definitions when explicit configuration is needed |
| | | # please note that last definitions always *replace* previous ones |
| | | # add more service definitions when explicit configuration is needed |
| | | # please note that last definitions always *replace* previous ones |
| | | 'App\EventListener\DeserializeListener': |
| | | tags: |
| | | - { name: 'kernel.event_listener', event: 'kernel.request', method: 'onKernelRequest', priority: 2 } |
| | | # Autoconfiguration must be disabled to set a custom priority |
| | | autoconfigure: false |
| | | decorates: 'api_platform.listener.request.deserialize' |
| | | arguments: |
| | | $decorated: '@App\EventListener\DeserializeListener.inner' |
| | | |
| | | 'App\DataTransformer\CreateSecretTransformer': ~ |
| | | # Uncomment only if autoconfiguration is disabled |
| | | #tags: [ 'api_platform.data_transformer' ] |
| | | App\Filter\PublicSecretQueryExtension: |
| | | tags: |
| | | - { name: api_platform.doctrine.orm.query_extension.collection } |
New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | |
| | | <!-- https://phpunit.readthedocs.io/en/latest/configuration.html --> |
| | | <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" |
| | | colors="true" |
| | | bootstrap="tests/bootstrap.php" |
| | | > |
| | | <php> |
| | | <ini name="display_errors" value="1" /> |
| | | <ini name="error_reporting" value="-1" /> |
| | | <server name="APP_ENV" value="test" force="true" /> |
| | | <server name="SHELL_VERBOSITY" value="-1" /> |
| | | <server name="SYMFONY_PHPUNIT_REMOVE" value="" /> |
| | | <server name="SYMFONY_PHPUNIT_VERSION" value="9.5" /> |
| | | </php> |
| | | |
| | | <testsuites> |
| | | <testsuite name="Project Test Suite"> |
| | | <directory>tests</directory> |
| | | </testsuite> |
| | | </testsuites> |
| | | |
| | | <coverage processUncoveredFiles="true"> |
| | | <include> |
| | | <directory suffix=".php">src</directory> |
| | | </include> |
| | | </coverage> |
| | | |
| | | <listeners> |
| | | <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" /> |
| | | </listeners> |
| | | |
| | | <!-- Run `composer require symfony/panther` before enabling this extension --> |
| | | <!-- |
| | | <extensions> |
| | | <extension class="Symfony\Component\Panther\ServerExtension" /> |
| | | </extensions> |
| | | --> |
| | | </phpunit> |
New file |
| | |
| | | <?php |
| | | |
| | | namespace App\Controller; |
| | | |
| | | use App\Entity\Secret; |
| | | use Doctrine\ORM\EntityManagerInterface; |
| | | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
| | | |
| | | /** |
| | | * We use this custom controller to decrement Secret after a GET operation |
| | | */ |
| | | class RetrieveSecretController extends AbstractController |
| | | { |
| | | /** |
| | | * @var EntityManagerInterface |
| | | */ |
| | | private $em; |
| | | |
| | | public function __construct(EntityManagerInterface $entityManager) { |
| | | $this->em = $entityManager; |
| | | } |
| | | |
| | | public function __invoke(Secret $data): Secret |
| | | { |
| | | $data->setRemainingViews($data->getRemainingViews() - 1); |
| | | $this->em->persist($data); |
| | | $this->em->flush(); |
| | | return $data; |
| | | } |
| | | } |
New file |
| | |
| | | <?php |
| | | |
| | | namespace App\DataTransformer; |
| | | |
| | | use ApiPlatform\Core\DataTransformer\DataTransformerInterface; |
| | | use ApiPlatform\Core\Validator\ValidatorInterface; |
| | | use App\Dto\CreateSecretDTO; |
| | | use App\Entity\Secret; |
| | | |
| | | class CreateSecretTransformer implements DataTransformerInterface |
| | | { |
| | | /** |
| | | * @var ValidatorInterface |
| | | */ |
| | | private $validator; |
| | | |
| | | public function __construct(ValidatorInterface $validator) |
| | | { |
| | | $this->validator = $validator; |
| | | } |
| | | |
| | | /** |
| | | * @inheritDoc |
| | | */ |
| | | public function transform($object, string $to, array $context = []) |
| | | { |
| | | $this->validator->validate($object); |
| | | |
| | | $expireInMinutes = $object->expireAfter > 0 ? $object->expireAfter : 0; |
| | | |
| | | $expireTime = null; |
| | | |
| | | if ($expireInMinutes) |
| | | { |
| | | $expireTime = (new \DateTimeImmutable()) |
| | | ->add(new \DateInterval('PT' . $expireInMinutes . 'M')); |
| | | } |
| | | |
| | | return (new Secret()) |
| | | ->setSecretText($object->secret) |
| | | ->setExpiresAt($expireTime) |
| | | ->setRemainingViews($object->expireAfterViews); |
| | | } |
| | | |
| | | /** |
| | | * @inheritDoc |
| | | */ |
| | | public function supportsTransformation($data, string $to, array $context = []): bool |
| | | { |
| | | if ($to !== Secret::class) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | if ($data instanceof CreateSecretDTO) |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | if (isset($data['secret']) && isset($data["expireAfterViews"])) |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | } |
New file |
| | |
| | | <?php |
| | | |
| | | namespace App\Dto; |
| | | |
| | | use ApiPlatform\Core\Annotation\ApiProperty; |
| | | use Symfony\Component\Validator\Constraints as Assert; |
| | | |
| | | final class CreateSecretDTO |
| | | { |
| | | /** |
| | | * @var string |
| | | * @ApiProperty(attributes={"openapi_context"={"description" = "This text will be saved as a secret"}}) |
| | | * @Assert\NotNull |
| | | * @Assert\NotBlank |
| | | */ |
| | | public $secret; |
| | | /** |
| | | * @var integer |
| | | * @ApiProperty(attributes={"openapi_context"={"description" = "The secret won't be available after the given number of views. It must be greater than 0."}}) |
| | | * @Assert\NotBlank |
| | | * @Assert\NotNull |
| | | * @Assert\Range( |
| | | * min=1 |
| | | * ) |
| | | */ |
| | | public $expireAfterViews; |
| | | |
| | | /** |
| | | * @var integer |
| | | * @ApiProperty(attributes={"openapi_context"={"description" = "The secret won't be available after the given time. The value is provided in minutes. 0 means never expires."}}) |
| | | * @Assert\NotBlank |
| | | * @Assert\NotNull |
| | | * @Assert\Range( |
| | | * min = 0 |
| | | * ) |
| | | */ |
| | | public $expireAfter; |
| | | |
| | | public static function fromArray($secret = null, $expireAfter = null, $expireAfterViews = null) |
| | | { |
| | | $s = new CreateSecretDTO(); |
| | | |
| | | $s->secret = $secret; |
| | | $s->expireAfter = $expireAfter; |
| | | $s->expireAfterViews = $expireAfterViews; |
| | | |
| | | return $s; |
| | | } |
| | | } |
New file |
| | |
| | | <?php |
| | | |
| | | namespace App\Entity; |
| | | |
| | | use ApiPlatform\Core\Annotation\ApiProperty; |
| | | use ApiPlatform\Core\Annotation\ApiResource; |
| | | use App\Repository\SecretRepository; |
| | | use Doctrine\ORM\Mapping as ORM; |
| | | use Symfony\Component\Validator\Constraints as Assert; |
| | | |
| | | /** |
| | | * @ApiResource( |
| | | * collectionOperations={ |
| | | * "create"={ |
| | | * "path"="secret", |
| | | * "method"="post", |
| | | * "input"=App\Dto\CreateSecretDTO::class, |
| | | * * "openapi_context": { |
| | | * "requestBody": { |
| | | * "content": { |
| | | * "application/x-www-form-urlencoded": { |
| | | * "schema": { |
| | | * "type": "object", |
| | | * "properties": { |
| | | * "secret": { |
| | | * "type": "string", |
| | | * "example": "https://i.pinimg.com/originals/e2/b8/e3/e2b8e32a0f9660e8fa0a2d68da618f0f.jpg", |
| | | * "description": "This text will be saved as a secret" |
| | | * }, |
| | | * "expireAfterViews": { |
| | | * "type": "integer", |
| | | * "example": 10, |
| | | * "description": "The secret won't be available after the given number of views. It must be greater than 0.", |
| | | * }, |
| | | * "expireAfter": { |
| | | * "type": "integer", |
| | | * "example": "100", |
| | | * "description": "The secret won't be available after the given time. The value is provided in minutes. 0 means never expires" |
| | | * }, |
| | | * }, |
| | | * }, |
| | | * }, |
| | | * }, |
| | | * }, |
| | | * }, |
| | | * } |
| | | * }, |
| | | * itemOperations={ |
| | | * "get"= |
| | | * { |
| | | * "path"="secret/{hash}", |
| | | * "controller"=\App\Controller\RetrieveSecretController::class |
| | | * }, |
| | | * } |
| | | * ) |
| | | * @ORM\Entity(repositoryClass=SecretRepository::class) |
| | | */ |
| | | class Secret |
| | | { |
| | | public function __construct() |
| | | { |
| | | $this->setCreatedAt(new \DateTimeImmutable()); |
| | | } |
| | | |
| | | /** |
| | | * @ORM\Id |
| | | * @ORM\Column(type="string", length=255, unique=true) |
| | | * @ApiProperty(attributes={"openapi_context"={"description" = "Unique hash to identify the secrets"}}) |
| | | * @Assert\NotNull |
| | | */ |
| | | private $hash; |
| | | |
| | | /** |
| | | * @ORM\Column(type="string", length=255) |
| | | * @ApiProperty(attributes={"openapi_context"={"description" = "The secret itself"}}) |
| | | * @Assert\NotNull |
| | | * @Assert\Length( |
| | | * min = 1, |
| | | * max = 255 |
| | | * ) |
| | | */ |
| | | private $secretText; |
| | | |
| | | /** |
| | | * @ORM\Column(type="datetime_immutable") |
| | | * @ApiProperty(attributes={"openapi_context"={"description" = "The date and time of the creation"}}) |
| | | * @Assert\NotNull |
| | | */ |
| | | private $createdAt; |
| | | |
| | | /** |
| | | * @ORM\Column(type="datetime_immutable", nullable=true) |
| | | * @ApiProperty(attributes={"openapi_context"={"description" = "The secret cannot be reached after this time"}}) |
| | | */ |
| | | private $expiresAt; |
| | | |
| | | /** |
| | | * @ORM\Column(type="integer") |
| | | * @ApiProperty(attributes={"openapi_context"={"description" = "How many times the secret can be viewed"}}) |
| | | * @Assert\NotNull |
| | | * @Assert\Range( |
| | | * min = 0 |
| | | * ) |
| | | */ |
| | | private $remainingViews; |
| | | |
| | | public function getHash(): ?string |
| | | { |
| | | return $this->hash; |
| | | } |
| | | |
| | | public function getSecretText(): ?string |
| | | { |
| | | return $this->secretText; |
| | | } |
| | | |
| | | public function setSecretText(string $secretText): self |
| | | { |
| | | $this->secretText = $secretText; |
| | | $this->hash = md5($secretText . uniqid('dont be too salty please', true)); |
| | | return $this; |
| | | } |
| | | |
| | | public function getCreatedAt(): ?\DateTimeImmutable |
| | | { |
| | | return $this->createdAt; |
| | | } |
| | | |
| | | public function setCreatedAt(\DateTimeImmutable $createdAt): self |
| | | { |
| | | $this->createdAt = $createdAt; |
| | | |
| | | return $this; |
| | | } |
| | | |
| | | public function getExpiresAt(): ?\DateTimeImmutable |
| | | { |
| | | return $this->expiresAt; |
| | | } |
| | | |
| | | public function setExpiresAt(?\DateTimeImmutable $expiresAt): self |
| | | { |
| | | $this->expiresAt = $expiresAt; |
| | | |
| | | return $this; |
| | | } |
| | | |
| | | public function getRemainingViews(): ?int |
| | | { |
| | | return $this->remainingViews; |
| | | } |
| | | |
| | | public function setRemainingViews(int $remainingViews): self |
| | | { |
| | | $this->remainingViews = $remainingViews; |
| | | |
| | | return $this; |
| | | } |
| | | } |
New file |
| | |
| | | <?php |
| | | |
| | | namespace App\EventListener; |
| | | |
| | | use ApiPlatform\Core\Util\RequestAttributesExtractor; |
| | | use ApiPlatform\Core\Validator\ValidatorInterface; |
| | | use Symfony\Component\HttpFoundation\Request; |
| | | use Symfony\Component\HttpKernel\Event\RequestEvent; |
| | | use ApiPlatform\Core\EventListener\DeserializeListener as DecoratedListener; |
| | | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; |
| | | use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; |
| | | |
| | | /** |
| | | * We need this to be able to accept application/x-www-form-urlencoded type data |
| | | */ |
| | | final class DeserializeListener |
| | | { |
| | | private $decorated; |
| | | private $denormalizer; |
| | | private $serializerContextBuilder; |
| | | |
| | | public function __construct( |
| | | DenormalizerInterface $denormalizer, |
| | | SerializerContextBuilderInterface $serializerContextBuilder, |
| | | DecoratedListener $decorated, |
| | | ValidatorInterface $validator |
| | | ) |
| | | { |
| | | $this->denormalizer = $denormalizer; |
| | | $this->serializerContextBuilder = $serializerContextBuilder; |
| | | $this->decorated = $decorated; |
| | | } |
| | | |
| | | public function onKernelRequest(RequestEvent $event): void |
| | | { |
| | | $request = $event->getRequest(); |
| | | if ($request->isMethodCacheable() || $request->isMethod(Request::METHOD_DELETE)) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | if ('form' === $request->getContentType()) |
| | | { |
| | | $this->denormalizeFormRequest($request); |
| | | } |
| | | else |
| | | { |
| | | $this->decorated->onKernelRequest($event); |
| | | } |
| | | } |
| | | |
| | | private function denormalizeFormRequest(Request $request): void |
| | | { |
| | | if (!$attributes = RequestAttributesExtractor::extractAttributes($request)) |
| | | { |
| | | return; |
| | | } |
| | | $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes); |
| | | $populated = $request->attributes->get('data'); |
| | | |
| | | if (null !== $populated) |
| | | { |
| | | $context['object_to_populate'] = $populated; |
| | | } |
| | | |
| | | $data = $request->request->all(); |
| | | $object = $this->denormalizer->denormalize($data, $attributes['resource_class'], null, $context); |
| | | $request->attributes->set('data', $object); |
| | | } |
| | | } |
New file |
| | |
| | | <?php |
| | | |
| | | namespace App\Filter; |
| | | |
| | | use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; |
| | | use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; |
| | | use App\Entity\Secret; |
| | | use Doctrine\ORM\QueryBuilder; |
| | | |
| | | /** |
| | | * This class restricts the items we can retrieve trough the API. |
| | | */ |
| | | class PublicSecretQueryExtension implements QueryItemExtensionInterface |
| | | { |
| | | public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []) |
| | | { |
| | | if ($resourceClass !== Secret::class) { |
| | | return; |
| | | } |
| | | $rootAlias = $queryBuilder->getRootAliases()[0]; |
| | | $queryBuilder |
| | | ->andWhere($rootAlias . ".expiresAt >= CURRENT_TIMESTAMP() OR " . $rootAlias . ".expiresAt is null") |
| | | ->andWhere($rootAlias . ".remainingViews > 0"); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | <?php |
| | | |
| | | namespace App\Repository; |
| | | |
| | | use App\Entity\Secret; |
| | | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; |
| | | use Doctrine\Persistence\ManagerRegistry; |
| | | |
| | | class SecretRepository extends ServiceEntityRepository |
| | | { |
| | | public function __construct(ManagerRegistry $registry) |
| | | { |
| | | parent::__construct($registry, Secret::class); |
| | | } |
| | | } |
New file |
| | |
| | | <?php |
| | | |
| | | namespace App\Serializer; |
| | | |
| | | use App\Dto\CreateSecretDTO; |
| | | use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; |
| | | |
| | | /** |
| | | * This denormalizer is required when we got the form in an x-www-form-urlencoded format, because we have to |
| | | * make a CreateSecretDTO object from the input POST data. |
| | | * |
| | | * After this, the regular (non-x-www-form-urlencoded) pipeline is active, validation & creation is handled by that. |
| | | */ |
| | | class SecretDenormalizer implements ContextAwareDenormalizerInterface |
| | | { |
| | | /** |
| | | * @inheritDoc |
| | | */ |
| | | public function supportsDenormalization($data, string $type, string $format = null, array $context = []) |
| | | { |
| | | if ($type !== CreateSecretDTO::class) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | return (isset($data['secret']) && isset($data['expireAfterViews']) && isset($data['expireAfter'])); |
| | | } |
| | | |
| | | /** |
| | | * @inheritDoc |
| | | */ |
| | | public function denormalize($data, string $type, string $format = null, array $context = []) |
| | | { |
| | | if ($type !== CreateSecretDTO::class) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | return CreateSecretDTO::fromArray($data['secret'], $data['expireAfter'], $data['expireAfterViews']); |
| | | } |
| | | } |
| | |
| | | { |
| | | "api-platform/core": { |
| | | "version": "2.6", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "2.5", |
| | | "ref": "05b57782a78c21a664a42055dc11cf1954ca36bb" |
| | | }, |
| | | "files": [ |
| | | "./config/routes/api_platform.yaml", |
| | | "./config/packages/api_platform.yaml", |
| | | "./src/Entity/.gitignore" |
| | | ] |
| | | }, |
| | | "composer/package-versions-deprecated": { |
| | | "version": "1.11.99.4" |
| | | }, |
| | | "doctrine/annotations": { |
| | | "version": "1.13", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "1.0", |
| | | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" |
| | | }, |
| | | "files": [ |
| | | "./config/routes/annotations.yaml" |
| | | ] |
| | | }, |
| | | "doctrine/cache": { |
| | | "version": "2.1.1" |
| | | }, |
| | | "doctrine/collections": { |
| | | "version": "1.6.8" |
| | | }, |
| | | "doctrine/common": { |
| | | "version": "3.2.0" |
| | | }, |
| | | "doctrine/dbal": { |
| | | "version": "3.1.3" |
| | | }, |
| | | "doctrine/deprecations": { |
| | | "version": "v0.5.3" |
| | | }, |
| | | "doctrine/doctrine-bundle": { |
| | | "version": "2.4", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "2.4", |
| | | "ref": "bac5c852ff628886de2753215fe5eb1f9ce980fb" |
| | | }, |
| | | "files": [ |
| | | "./config/packages/doctrine.yaml", |
| | | "./config/packages/test/doctrine.yaml", |
| | | "./config/packages/prod/doctrine.yaml", |
| | | "./src/Repository/.gitignore", |
| | | "./src/Entity/.gitignore" |
| | | ] |
| | | }, |
| | | "doctrine/doctrine-migrations-bundle": { |
| | | "version": "3.1", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "3.1", |
| | | "ref": "ee609429c9ee23e22d6fa5728211768f51ed2818" |
| | | }, |
| | | "files": [ |
| | | "./config/packages/doctrine_migrations.yaml", |
| | | "./migrations/.gitignore" |
| | | ] |
| | | }, |
| | | "doctrine/event-manager": { |
| | | "version": "1.1.1" |
| | | }, |
| | | "doctrine/inflector": { |
| | | "version": "2.0.3" |
| | | }, |
| | | "doctrine/instantiator": { |
| | | "version": "1.4.0" |
| | | }, |
| | | "doctrine/lexer": { |
| | | "version": "1.2.1" |
| | | }, |
| | | "doctrine/migrations": { |
| | | "version": "3.3.0" |
| | | }, |
| | | "doctrine/orm": { |
| | | "version": "2.10.1" |
| | | }, |
| | | "doctrine/persistence": { |
| | | "version": "2.2.2" |
| | | }, |
| | | "doctrine/sql-formatter": { |
| | | "version": "1.1.1" |
| | | }, |
| | | "fig/link-util": { |
| | | "version": "1.1.2" |
| | | }, |
| | | "friendsofphp/proxy-manager-lts": { |
| | | "version": "v1.0.5" |
| | | }, |
| | | "justinrainbow/json-schema": { |
| | | "version": "5.2.11" |
| | | }, |
| | | "laminas/laminas-code": { |
| | | "version": "4.4.3" |
| | | }, |
| | | "myclabs/deep-copy": { |
| | | "version": "1.10.2" |
| | | }, |
| | | "nelmio/cors-bundle": { |
| | | "version": "2.1", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "1.5", |
| | | "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" |
| | | }, |
| | | "files": [ |
| | | "./config/packages/nelmio_cors.yaml" |
| | | ] |
| | | }, |
| | | "nikic/php-parser": { |
| | | "version": "v4.13.0" |
| | | }, |
| | | "phar-io/manifest": { |
| | | "version": "2.0.3" |
| | | }, |
| | | "phar-io/version": { |
| | | "version": "3.1.0" |
| | | }, |
| | | "phpdocumentor/reflection-common": { |
| | | "version": "2.2.0" |
| | | }, |
| | | "phpdocumentor/reflection-docblock": { |
| | | "version": "5.2.2" |
| | | }, |
| | | "phpdocumentor/type-resolver": { |
| | | "version": "1.5.1" |
| | | }, |
| | | "phpspec/prophecy": { |
| | | "version": "1.14.0" |
| | | }, |
| | | "phpunit/php-code-coverage": { |
| | | "version": "9.2.7" |
| | | }, |
| | | "phpunit/php-file-iterator": { |
| | | "version": "3.0.5" |
| | | }, |
| | | "phpunit/php-invoker": { |
| | | "version": "3.1.1" |
| | | }, |
| | | "phpunit/php-text-template": { |
| | | "version": "2.0.4" |
| | | }, |
| | | "phpunit/php-timer": { |
| | | "version": "5.0.3" |
| | | }, |
| | | "phpunit/phpunit": { |
| | | "version": "9.5", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "9.3", |
| | | "ref": "a6249a6c4392e9169b87abf93225f7f9f59025e6" |
| | | }, |
| | | "files": [ |
| | | "./.env.test", |
| | | "./phpunit.xml.dist", |
| | | "./tests/bootstrap.php" |
| | | ] |
| | | }, |
| | | "psr/cache": { |
| | | "version": "1.0.1" |
| | | }, |
| | |
| | | "psr/event-dispatcher": { |
| | | "version": "1.0.0" |
| | | }, |
| | | "psr/link": { |
| | | "version": "1.0.0" |
| | | }, |
| | | "psr/log": { |
| | | "version": "1.1.4" |
| | | }, |
| | | "sebastian/cli-parser": { |
| | | "version": "1.0.1" |
| | | }, |
| | | "sebastian/code-unit": { |
| | | "version": "1.0.8" |
| | | }, |
| | | "sebastian/code-unit-reverse-lookup": { |
| | | "version": "2.0.3" |
| | | }, |
| | | "sebastian/comparator": { |
| | | "version": "4.0.6" |
| | | }, |
| | | "sebastian/complexity": { |
| | | "version": "2.0.2" |
| | | }, |
| | | "sebastian/diff": { |
| | | "version": "4.0.4" |
| | | }, |
| | | "sebastian/environment": { |
| | | "version": "5.1.3" |
| | | }, |
| | | "sebastian/exporter": { |
| | | "version": "4.0.3" |
| | | }, |
| | | "sebastian/global-state": { |
| | | "version": "5.0.3" |
| | | }, |
| | | "sebastian/lines-of-code": { |
| | | "version": "1.0.3" |
| | | }, |
| | | "sebastian/object-enumerator": { |
| | | "version": "4.0.4" |
| | | }, |
| | | "sebastian/object-reflector": { |
| | | "version": "2.0.4" |
| | | }, |
| | | "sebastian/recursion-context": { |
| | | "version": "4.0.4" |
| | | }, |
| | | "sebastian/resource-operations": { |
| | | "version": "3.0.3" |
| | | }, |
| | | "sebastian/type": { |
| | | "version": "2.3.4" |
| | | }, |
| | | "sebastian/version": { |
| | | "version": "3.0.2" |
| | | }, |
| | | "symfony/asset": { |
| | | "version": "v5.3.4" |
| | | }, |
| | | "symfony/browser-kit": { |
| | | "version": "v5.3.4" |
| | | }, |
| | | "symfony/cache": { |
| | | "version": "v5.3.8" |
| | |
| | | "./bin/console" |
| | | ] |
| | | }, |
| | | "symfony/css-selector": { |
| | | "version": "v5.3.4" |
| | | }, |
| | | "symfony/dependency-injection": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/deprecation-contracts": { |
| | | "version": "v2.4.0" |
| | | }, |
| | | "symfony/doctrine-bridge": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/dom-crawler": { |
| | | "version": "v5.3.7" |
| | | }, |
| | | "symfony/dotenv": { |
| | | "version": "v5.3.8" |
| | |
| | | }, |
| | | "symfony/event-dispatcher-contracts": { |
| | | "version": "v2.4.0" |
| | | }, |
| | | "symfony/expression-language": { |
| | | "version": "v5.3.7" |
| | | }, |
| | | "symfony/filesystem": { |
| | | "version": "v5.3.4" |
| | |
| | | "./src/Controller/.gitignore" |
| | | ] |
| | | }, |
| | | "symfony/http-client": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/http-client-contracts": { |
| | | "version": "v2.4.0" |
| | | }, |
| | |
| | | }, |
| | | "symfony/http-kernel": { |
| | | "version": "v5.3.9" |
| | | }, |
| | | "symfony/maker-bundle": { |
| | | "version": "1.34", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "1.0", |
| | | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" |
| | | } |
| | | }, |
| | | "symfony/password-hasher": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/phpunit-bridge": { |
| | | "version": "5.3", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "5.3", |
| | | "ref": "97cb3dc7b0f39c7cfc4b7553504c9d7b7795de96" |
| | | }, |
| | | "files": [ |
| | | "./.env.test", |
| | | "./bin/phpunit", |
| | | "./phpunit.xml.dist", |
| | | "./tests/bootstrap.php" |
| | | ] |
| | | }, |
| | | "symfony/polyfill-intl-grapheme": { |
| | | "version": "v1.23.1" |
| | |
| | | "symfony/polyfill-php81": { |
| | | "version": "v1.23.0" |
| | | }, |
| | | "symfony/property-access": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/property-info": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/proxy-manager-bridge": { |
| | | "version": "v5.3.4" |
| | | }, |
| | | "symfony/routing": { |
| | | "version": "5.3", |
| | | "recipe": { |
| | |
| | | "symfony/runtime": { |
| | | "version": "v5.3.4" |
| | | }, |
| | | "symfony/security-bundle": { |
| | | "version": "5.3", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "5.3", |
| | | "ref": "9c4fcf79873f7400c885b90935f7163233615d6f" |
| | | }, |
| | | "files": [ |
| | | "./config/packages/security.yaml" |
| | | ] |
| | | }, |
| | | "symfony/security-core": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/security-csrf": { |
| | | "version": "v5.3.4" |
| | | }, |
| | | "symfony/security-guard": { |
| | | "version": "v5.3.7" |
| | | }, |
| | | "symfony/security-http": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/serializer": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/service-contracts": { |
| | | "version": "v2.4.0" |
| | | }, |
| | | "symfony/stopwatch": { |
| | | "version": "v5.3.4" |
| | | }, |
| | | "symfony/string": { |
| | | "version": "v5.3.7" |
| | | }, |
| | | "symfony/translation-contracts": { |
| | | "version": "v2.4.0" |
| | | }, |
| | | "symfony/twig-bridge": { |
| | | "version": "v5.3.7" |
| | | }, |
| | | "symfony/twig-bundle": { |
| | | "version": "5.3", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "5.3", |
| | | "ref": "3dd530739a4284e3272274c128dbb7a8140a66f1" |
| | | }, |
| | | "files": [ |
| | | "./config/packages/twig.yaml", |
| | | "./templates/base.html.twig" |
| | | ] |
| | | }, |
| | | "symfony/validator": { |
| | | "version": "5.3", |
| | | "recipe": { |
| | | "repo": "github.com/symfony/recipes", |
| | | "branch": "master", |
| | | "version": "4.3", |
| | | "ref": "3eb8df139ec05414489d55b97603c5f6ca0c44cb" |
| | | }, |
| | | "files": [ |
| | | "./config/packages/validator.yaml", |
| | | "./config/packages/test/validator.yaml" |
| | | ] |
| | | }, |
| | | "symfony/var-dumper": { |
| | | "version": "v5.3.8" |
| | |
| | | "symfony/var-exporter": { |
| | | "version": "v5.3.8" |
| | | }, |
| | | "symfony/web-link": { |
| | | "version": "v5.3.4" |
| | | }, |
| | | "symfony/yaml": { |
| | | "version": "v5.3.6" |
| | | }, |
| | | "theseer/tokenizer": { |
| | | "version": "1.2.1" |
| | | }, |
| | | "twig/twig": { |
| | | "version": "v3.3.3" |
| | | }, |
| | | "webmozart/assert": { |
| | | "version": "1.10.0" |
| | | }, |
| | | "willdurand/negotiation": { |
| | | "version": "3.0.0" |
| | | } |
| | | } |
New file |
| | |
| | | <?php |
| | | |
| | | namespace App\Tests; |
| | | |
| | | use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase; |
| | | use App\Entity\Secret; |
| | | |
| | | class SecretServerTest extends ApiTestCase |
| | | { |
| | | private $secretValidDtoFixture = ['secret' => 'Something very secretly created stuff', 'expireAfterViews' => 10, 'expireAfter' => 10]; |
| | | |
| | | public function testDefaultCollectionRoutes(): void |
| | | { |
| | | $client = static::createClient(); |
| | | |
| | | $client->request('GET', 'v1/secrets'); |
| | | $this->assertResponseStatusCodeSame(404); |
| | | |
| | | $client->request('GET', 'v1/secret/asdf'); |
| | | $this->assertResponseStatusCodeSame(404); |
| | | } |
| | | |
| | | public function testInvalidSecretFormats() |
| | | { |
| | | $client = static::createClient(); |
| | | |
| | | $invalidFixture = $this->secretValidDtoFixture; |
| | | $invalidFixture['expireAfterViews'] = 'sajt'; |
| | | |
| | | $client->request( |
| | | 'POST', |
| | | 'v1/secret', |
| | | [ |
| | | 'json' => $invalidFixture |
| | | ] |
| | | ); |
| | | //missing field, invalid field, etc |
| | | $this->assertResponseStatusCodeSame(405); |
| | | |
| | | unset($invalidFixture['expireAfterViews']); |
| | | |
| | | $client->request( |
| | | 'POST', |
| | | 'v1/secret', |
| | | [ |
| | | 'json' => $invalidFixture |
| | | ] |
| | | ); |
| | | //missing field, invalid field, etc |
| | | $this->assertResponseStatusCodeSame(405); |
| | | } |
| | | |
| | | public function testValidSecretCreation() |
| | | { |
| | | $client = static::createClient(); |
| | | |
| | | $response = $client->request( |
| | | 'POST', |
| | | 'v1/secret', |
| | | [ |
| | | 'json' => $this->secretValidDtoFixture |
| | | ] |
| | | ); |
| | | //should be created |
| | | $this->assertResponseStatusCodeSame(201); |
| | | |
| | | $content = $response->getContent(); |
| | | $this->assertMatchesJsonSchema(['hash', 'secretText', 'createdAt', 'expiresAt', 'remainingViews']); |
| | | $objectAsArray = json_decode($content, true); |
| | | |
| | | $this->assertEquals($this->secretValidDtoFixture['secret'], $objectAsArray['secretText']); |
| | | $this->assertEquals($this->secretValidDtoFixture['expireAfterViews'], $objectAsArray['remainingViews']); |
| | | |
| | | $response = $client->request('GET', 'v1/secret/' . $objectAsArray['hash']); |
| | | |
| | | $this->assertResponseStatusCodeSame(200); |
| | | $content = $response->getContent(); |
| | | //we haven't saw this in the previous endpoint, we saw the creation response, so it worth to do the same check |
| | | $this->assertMatchesJsonSchema(['hash', 'secretText', 'createdAt', 'expiresAt', 'remainingViews']); |
| | | $objectAsArray = json_decode($content, true); |
| | | |
| | | $this->assertEquals($this->secretValidDtoFixture['expireAfterViews'] - 1, $objectAsArray['remainingViews']); |
| | | } |
| | | } |
New file |
| | |
| | | <?php |
| | | |
| | | use Symfony\Component\Dotenv\Dotenv; |
| | | |
| | | require dirname(__DIR__).'/vendor/autoload.php'; |
| | | |
| | | if (file_exists(dirname(__DIR__).'/config/bootstrap.php')) { |
| | | require dirname(__DIR__).'/config/bootstrap.php'; |
| | | } elseif (method_exists(Dotenv::class, 'bootEnv')) { |
| | | (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); |
| | | } |