diff --git a/.idea/TijdVoorDeTest.iml b/.idea/TijdVoorDeTest.iml index 2922a68..325953a 100644 --- a/.idea/TijdVoorDeTest.iml +++ b/.idea/TijdVoorDeTest.iml @@ -164,6 +164,8 @@ + + diff --git a/.idea/php.xml b/.idea/php.xml index 84b5fd3..7e4821b 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -199,6 +199,8 @@ + + diff --git a/assets/backoffice.js b/assets/backoffice.js index 530abfb..dcb5730 100644 --- a/assets/backoffice.js +++ b/assets/backoffice.js @@ -1,3 +1,4 @@ +import './bootstrap.js'; import 'bootstrap/dist/css/bootstrap.min.css' import * as bootstrap from 'bootstrap' diff --git a/assets/bootstrap.js b/assets/bootstrap.js new file mode 100644 index 0000000..d4e50c9 --- /dev/null +++ b/assets/bootstrap.js @@ -0,0 +1,5 @@ +import { startStimulusApp } from '@symfony/stimulus-bundle'; + +const app = startStimulusApp(); +// register any custom, 3rd party controllers here +// app.register('some_controller_name', SomeImportedController); diff --git a/assets/controllers.json b/assets/controllers.json new file mode 100644 index 0000000..29ea244 --- /dev/null +++ b/assets/controllers.json @@ -0,0 +1,15 @@ +{ + "controllers": { + "@symfony/ux-turbo": { + "turbo-core": { + "enabled": true, + "fetch": "eager" + }, + "mercure-turbo-stream": { + "enabled": false, + "fetch": "eager" + } + } + }, + "entrypoints": [] +} diff --git a/assets/controllers/csrf_protection_controller.js b/assets/controllers/csrf_protection_controller.js new file mode 100644 index 0000000..2811f21 --- /dev/null +++ b/assets/controllers/csrf_protection_controller.js @@ -0,0 +1,79 @@ +const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/; +const tokenCheck = /^[-_\/+a-zA-Z0-9]{24,}$/; + +// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager +document.addEventListener('submit', function (event) { + generateCsrfToken(event.target); +}, true); + +// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie +// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked +document.addEventListener('turbo:submit-start', function (event) { + const h = generateCsrfHeaders(event.detail.formSubmission.formElement); + Object.keys(h).map(function (k) { + event.detail.formSubmission.fetchRequest.headers[k] = h[k]; + }); +}); + +// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted +document.addEventListener('turbo:submit-end', function (event) { + removeCsrfToken(event.detail.formSubmission.formElement); +}); + +export function generateCsrfToken (formElement) { + const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]'); + + if (!csrfField) { + return; + } + + let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); + let csrfToken = csrfField.value; + + if (!csrfCookie && nameCheck.test(csrfToken)) { + csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken); + csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18)))); + csrfField.dispatchEvent(new Event('change', { bubbles: true })); + } + + if (csrfCookie && tokenCheck.test(csrfToken)) { + const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict'; + document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie; + } +} + +export function generateCsrfHeaders (formElement) { + const headers = {}; + const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]'); + + if (!csrfField) { + return headers; + } + + const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); + + if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) { + headers[csrfCookie] = csrfField.value; + } + + return headers; +} + +export function removeCsrfToken (formElement) { + const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]'); + + if (!csrfField) { + return; + } + + const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); + + if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) { + const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0'; + + document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie; + } +} + +/* stimulusFetch: 'lazy' */ +export default 'csrf-protection-controller'; diff --git a/assets/quiz.js b/assets/quiz.js index 488df76..f7c2c54 100644 --- a/assets/quiz.js +++ b/assets/quiz.js @@ -1,3 +1,4 @@ +import './bootstrap.js'; import 'bootstrap/dist/css/bootstrap.min.css' import * as bootstrap from 'bootstrap' diff --git a/composer.json b/composer.json index f3eb576..8551e0f 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "symfony/serializer": "7.2.*", "symfony/twig-bundle": "7.2.*", "symfony/uid": "7.2.*", + "symfony/ux-turbo": "^2.26", "symfony/yaml": "7.2.*", "symfonycasts/sass-bundle": "^0.8.2", "symfonycasts/verify-email-bundle": "^1.17.3", diff --git a/composer.lock b/composer.lock index 9176628..1fe063b 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": "a42d81785aafd03b84dac3f6d813d5c2", + "content-hash": "9a4a40083e24d7e3f49ad01ace830440", "packages": [ { "name": "composer/pcre", @@ -6614,6 +6614,75 @@ ], "time": "2024-09-25T14:20:29+00:00" }, + { + "name": "symfony/stimulus-bundle", + "version": "v2.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stimulus-bundle.git", + "reference": "750c770f66b45b40135eb32d753dbd7d84182c92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/750c770f66b45b40135eb32d753dbd7d84182c92", + "reference": "750c770f66b45b40135eb32d753dbd7d84182c92", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.0|^3.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "twig/twig": "^2.15.3|^3.8" + }, + "require-dev": { + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "zenstruck/browser": "^1.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\UX\\StimulusBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Stimulus!", + "keywords": [ + "symfony-ux" + ], + "support": { + "source": "https://github.com/symfony/stimulus-bundle/tree/v2.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-23T09:27:30+00:00" + }, { "name": "symfony/stopwatch", "version": "v7.2.4", @@ -7279,6 +7348,105 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/ux-turbo", + "version": "v2.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-turbo.git", + "reference": "12d9989fb945f2074b41aad4915fc593ecd4ed00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/12d9989fb945f2074b41aad4915fc593ecd4ed00", + "reference": "12d9989fb945f2074b41aad4915fc593ecd4ed00", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/stimulus-bundle": "^2.9.1" + }, + "conflict": { + "symfony/flex": "<1.13" + }, + "require-dev": { + "dbrekelmans/bdi": "dev-main", + "doctrine/doctrine-bundle": "^2.4.3", + "doctrine/orm": "^2.8 | 3.0", + "php-webdriver/webdriver": "^1.15", + "phpstan/phpstan": "^2.1.17", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/debug-bundle": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/mercure-bundle": "^0.3.7", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/panther": "^2.2", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|6.3.*|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "symfony/ux-twig-component": "^2.21", + "symfony/web-profiler-bundle": "^5.4|^6.0|^7.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ux", + "name": "symfony/ux" + } + }, + "autoload": { + "psr-4": { + "Symfony\\UX\\Turbo\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Hotwire Turbo integration for Symfony", + "homepage": "https://symfony.com", + "keywords": [ + "hotwire", + "javascript", + "mercure", + "symfony-ux", + "turbo", + "turbo-stream" + ], + "support": { + "source": "https://github.com/symfony/ux-turbo/tree/v2.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-26T20:34:50+00:00" + }, { "name": "symfony/ux-twig-component", "version": "v2.25.2", diff --git a/config/bundles.php b/config/bundles.php index 9fcc9db..c5e0675 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -12,6 +12,8 @@ use Symfony\Bundle\MakerBundle\MakerBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; +use Symfony\UX\StimulusBundle\StimulusBundle; +use Symfony\UX\Turbo\TurboBundle; use Symfony\UX\TwigComponent\TwigComponentBundle; use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle; use Symfonycasts\SassBundle\SymfonycastsSassBundle; @@ -32,4 +34,6 @@ return [ SymfonyCastsVerifyEmailBundle::class => ['all' => true], SentryBundle::class => ['prod' => true], SymfonycastsSassBundle::class => ['all' => true], + StimulusBundle::class => ['all' => true], + TurboBundle::class => ['all' => true], ]; diff --git a/importmap.php b/importmap.php index 1f71f6d..56f5dc6 100644 --- a/importmap.php +++ b/importmap.php @@ -32,4 +32,13 @@ return [ 'version' => '5.3.6', 'type' => 'css', ], + '@hotwired/stimulus' => [ + 'version' => '3.2.2', + ], + '@symfony/stimulus-bundle' => [ + 'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js', + ], + '@hotwired/turbo' => [ + 'version' => '7.3.0', + ], ]; diff --git a/src/Controller/QuizController.php b/src/Controller/QuizController.php index b51aedc..12dd24f 100644 --- a/src/Controller/QuizController.php +++ b/src/Controller/QuizController.php @@ -110,6 +110,8 @@ final class QuizController extends AbstractController ->setAnswer($answer) ->setQuiz($answer->getQuestion()->getQuiz()); $givenAnswerRepository->save($givenAnswer); + + return $this->redirectToRoute('app_quiz_quizpage', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => $nameHash]); } $question = $questionRepository->findNextQuestionForCandidate($candidate); diff --git a/symfony.lock b/symfony.lock index 6cb1ce2..9c3b93e 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,4 +1,13 @@ { + "doctrine/deprecations": { + "version": "1.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "87424683adc81d7dc305eefec1fced883084aab9" + } + }, "doctrine/doctrine-bundle": { "version": "2.13", "recipe": { @@ -230,6 +239,21 @@ "config/routes/security.yaml" ] }, + "symfony/stimulus-bundle": { + "version": "2.26", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.20", + "ref": "3acc494b566816514a6873a89023a35440b6386d" + }, + "files": [ + "assets/bootstrap.js", + "assets/controllers.json", + "assets/controllers/csrf_protection_controller.js", + "assets/controllers/hello_controller.js" + ] + }, "symfony/translation": { "version": "7.2", "recipe": { @@ -265,6 +289,15 @@ "ref": "0df5844274d871b37fc3816c57a768ffc60a43a5" } }, + "symfony/ux-turbo": { + "version": "2.26", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.20", + "ref": "c85ff94da66841d7ff087c19cbcd97a2df744ef9" + } + }, "symfony/ux-twig-component": { "version": "2.22", "recipe": {