From 933dc424c359edfc5c8cf1371ea712a09dd26fd9 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Sep 2025 14:05:45 +0200 Subject: [PATCH] Setup to allow creating snipped from API --- .env | 4 ++ composer.json | 1 + composer.lock | 64 ++++++++++++++++++- config/bundles.php | 1 + config/packages/nelmio_cors.yaml | 15 +++++ src/Controller/Api/ExtensionController.php | 42 ++++++++++++ src/Controller/SnipController.php | 18 +++++- src/Entity/Snip.php | 2 +- .../SnipParser/Generic/UnsafeParser.php | 13 ++++ symfony.lock | 12 ++++ 10 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 config/packages/nelmio_cors.yaml create mode 100644 src/Controller/Api/ExtensionController.php create mode 100644 src/Service/SnipParser/Generic/UnsafeParser.php diff --git a/.env b/.env index 703e18e..23747da 100644 --- a/.env +++ b/.env @@ -27,3 +27,7 @@ APP_SECRET= # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.13-MariaDB&charset=utf8mb4" # DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8" ###< doctrine/doctrine-bundle ### + +###> nelmio/cors-bundle ### +CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' +###< nelmio/cors-bundle ### diff --git a/composer.json b/composer.json index afed29a..5e58303 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "doctrine/orm": "^3.4", "league/commonmark": "^2.6", "league/pipeline": "^1.0", + "nelmio/cors-bundle": "^2.5", "phpdocumentor/reflection-docblock": "^5.6", "phpstan/phpdoc-parser": "^2.1", "symfony/asset": "*", diff --git a/composer.lock b/composer.lock index 6dec8eb..3dbd252 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "405d2a2709cb499d92b64659d2f59b2e", + "content-hash": "b11b8d3b530bf74510557e53988cdf75", "packages": [ { "name": "dflydev/dot-access-data", @@ -1548,6 +1548,68 @@ ], "time": "2025-03-24T10:02:05+00:00" }, + { + "name": "nelmio/cors-bundle", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.6", + "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.5.0" + }, + "time": "2024-06-24T21:25:28+00:00" + }, { "name": "nette/schema", "version": "v1.3.2", diff --git a/config/bundles.php b/config/bundles.php index c5a5736..8f3632a 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -11,4 +11,5 @@ return [ Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], ]; diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml new file mode 100644 index 0000000..8d7b402 --- /dev/null +++ b/config/packages/nelmio_cors.yaml @@ -0,0 +1,15 @@ +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: + '^/api/': + allow_origin: ['*'] + allow_headers: ['*'] + allow_methods: ['POST', 'PUT', 'GET', 'DELETE'] + max_age: 3600 + '^/': null diff --git a/src/Controller/Api/ExtensionController.php b/src/Controller/Api/ExtensionController.php new file mode 100644 index 0000000..365acb2 --- /dev/null +++ b/src/Controller/Api/ExtensionController.php @@ -0,0 +1,42 @@ +getContent(), true); + if (!$data || !isset($data['html'])) { + return $this->errorResponse('Invalid JSON payload: Missing html parameter'); + } + $html = $data['html']; + + // Store HTML in cache with unique key + $userId = $this->getUser()?->getId(); + $cacheKey = 'html_' . $userId . '_' . bin2hex(random_bytes(8)); + $cache->get($cacheKey, function () use ($html) { + // value to cache + return $html; + }); + + // Build URL to redirect user to a form page later + $formUrl = $this->generateUrl( + 'snip_new', + ['cacheKey' => $cacheKey], + UrlGeneratorInterface::ABSOLUTE_URL + ); + + return $this->successResponse(['url' => $formUrl]); + } +} diff --git a/src/Controller/SnipController.php b/src/Controller/SnipController.php index df62fa0..c0a0732 100644 --- a/src/Controller/SnipController.php +++ b/src/Controller/SnipController.php @@ -5,6 +5,8 @@ namespace App\Controller; use App\Controller\Attribute\MapQueryCached; use App\Dto\SnipFilterRequest; use App\Entity\Snip; +use App\Entity\SnipContent; +use App\Entity\Tag; use App\Form\ConfirmationType; use App\Form\SnipType; use App\Repository\SnipRepository; @@ -16,6 +18,7 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Contracts\Cache\CacheInterface; #[Route('/snip', name: 'snip')] class SnipController extends AbstractController @@ -121,8 +124,13 @@ class SnipController extends AbstractController ]); } - #[Route('/new', name: '_new')] - public function new(Request $request, SnipContentService $contentService): Response + #[Route('/new/{cacheKey}', name: '_new')] + public function new( + Request $request, + SnipContentService $contentService, + CacheInterface $cache, + ?string $cacheKey = null, + ): Response { $snip = new Snip(); $snip->setCreatedAtNow(); @@ -131,6 +139,12 @@ class SnipController extends AbstractController $form = $this->createForm(SnipType::class, $snip); $form->add('Create', SubmitType::class); + if ($cacheKey) { + $content = $cache->get($cacheKey, fn() => null); + $form->get('content')->setData($content); + $form->get('parser')->setData('unsafe'); + } + $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->repository->save($snip); diff --git a/src/Entity/Snip.php b/src/Entity/Snip.php index d74ca8c..ac9f676 100644 --- a/src/Entity/Snip.php +++ b/src/Entity/Snip.php @@ -25,7 +25,7 @@ class Snip #[ORM\Column] public bool $public = false; - #[ORM\OneToMany(mappedBy: 'snip', targetEntity: SnipContent::class, orphanRemoval: true)] + #[ORM\OneToMany(targetEntity: SnipContent::class, mappedBy: 'snip', orphanRemoval: true)] public Collection $snipContents; #[ORM\OneToOne] diff --git a/src/Service/SnipParser/Generic/UnsafeParser.php b/src/Service/SnipParser/Generic/UnsafeParser.php new file mode 100644 index 0000000..fe39d35 --- /dev/null +++ b/src/Service/SnipParser/Generic/UnsafeParser.php @@ -0,0 +1,13 @@ +