CVE-2023-46245: Other (NVD-CWE-Other)
value: Kimai is a web-based multi-user time-tracking application. Versions prior to 2.1.0 are vulnerable to a Server-Side Template Injection (SSTI) which can be escalated to Remote Code Execution (RCE). The vulnerability arises when a malicious user uploads a specially crafted Twig file, exploiting the software's PDF and HTML rendering functionalities. Version 2.1.0 enables security measures for custom Twig templates.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 7.2
Published: 2023-10-31T16:15Z
Repository: https://github.com/kimai/kimai
Language: PHP
Files changed: .gitignore, composer.json, composer.lock, config/packages/doctrine.yaml, config/packages/framework.yaml, config/packages/kimai.yaml, config/packages/security.yaml, config/routes.yaml, config/services.yaml, phpstan.neon, src/API/ActionsController.php, src/API/ActivityController.php, src/API/BaseApiController.php, src/API/CustomerController.php, src/API/ProjectController.php, src/API/TimesheetController.php, src/API/UserController.php, src/Activity/ActivityService.php, src/Calendar/CalendarService.php, src/Command/DemoteUserCommand.php, src/Command/InvoiceCreateCommand.php, src/Command/UserLoginLinkCommand.php, src/Constants.php, src/Controller/AbstractController.php, src/Controller/ActivityController.php, src/Controller/ContractController.php, src/Controller/CustomerController.php, src/Controller/DashboardController.php, src/Controller/HomepageController.php, src/Controller/InvoiceController.php, src/Controller/PermissionController.php, src/Controller/ProfileController.php, src/Controller/ProjectController.php, src/Controller/Reporting/ReportUsersMonthController.php, src/Controller/Reporting/ReportUsersWeekController.php, src/Controller/Reporting/ReportUsersYearController.php, src/Controller/Reporting/UserMonthController.php, src/Controller/Reporting/UserWeekController.php, src/Controller/Reporting/UserYearController.php, src/Controller/Security/LoginLinkController.php, src/Controller/Security/PasswordResetController.php, src/Controller/Security/SelfRegistrationController.php, src/Controller/SystemConfigurationController.php, src/Controller/TimesheetAbstractController.php, src/Controller/UserController.php, src/Controller/WizardController.php, src/Customer/CustomerService.php, src/DependencyInjection/Configuration.php, src/Doctrine/ModifiedSubscriber.php, src/Doctrine/TimesheetSubscriber.php, src/Entity/Activity.php, src/Entity/ActivityMeta.php, src/Entity/ActivityRate.php, src/Entity/Bookmark.php, src/Entity/CommentTableTypeTrait.php, src/Entity/Customer.php, src/Entity/CustomerComment.php, src/Entity/CustomerMeta.php, src/Entity/CustomerRate.php, src/Entity/Invoice.php, src/Entity/InvoiceMeta.php, src/Entity/Project.php, src/Entity/ProjectComment.php, src/Entity/ProjectMeta.php, src/Entity/ProjectRate.php, src/Entity/Rate.php, src/Entity/RolePermission.php, src/Entity/Tag.php, src/Entity/Team.php, src/Entity/TeamMember.php, src/Entity/Timesheet.php, src/Entity/TimesheetMeta.php, src/Entity/User.php, src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php, src/EventSubscriber/MenuBuilderSubscriber.php, src/EventSubscriber/UserPreferenceSubscriber.php, src/EventSubscriber/UserProfileSubscriber.php, src/EventSubscriber/WizardSubscriber.php, src/Export/Base/AbstractSpreadsheetRenderer.php, src/Export/Base/HtmlRenderer.php, src/Export/Base/PDFRenderer.php, src/Export/Renderer/HtmlRendererFactory.php, src/Export/ServiceExport.php, src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php, src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php, src/Form/MultiUpdate/TimesheetMultiUpdate.php, src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php, src/Form/Type/MenuChoiceType.php, src/Form/Type/SkinType.php, src/Form/UserCreateType.php, src/Invoice/ServiceInvoice.php, src/Kernel.php, src/Project/ProjectService.php, src/Project/ProjectStatisticService.php, src/Reporting/ReportingService.php, src/Repository/TimesheetRepository.php, src/Security/TwoFactorCondition.php, src/Timesheet/TimesheetService.php, src/Twig/Extensions.php, src/Twig/LocaleFormatExtensions.php, src/Twig/PaginationExtension.php, src/Twig/SecurityPolicy/ChainPolicy.php, src/Twig/SecurityPolicy/DefaultPolicy.php, src/Twig/SecurityPolicy/ExportPolicy.php, src/Twig/SecurityPolicy/ForbiddenPolicy.php, src/Twig/SecurityPolicy/InvoicePolicy.php, src/Twig/TwigRendererTrait.php, src/User/LoginManager.php, src/User/UserService.php, src/Widget/Type/AbstractActiveUsers.php, src/Widget/Type/AbstractAmountPeriod.php, src/Widget/Type/AbstractCounterDuration.php, src/Widget/Type/AbstractCounterYear.php, src/Widget/Type/AbstractUserRevenuePeriod.php, src/Widget/Type/AbstractWidget.php, src/Widget/Type/ActiveTimesheets.php, src/Widget/Type/ActiveUsersMonth.php, src/Widget/Type/ActiveUsersToday.php, src/Widget/Type/ActiveUsersTotal.php, src/Widget/Type/ActiveUsersWeek.php, src/Widget/Type/ActiveUsersYear.php, src/Widget/Type/AmountMonth.php, src/Widget/Type/AmountToday.php, src/Widget/Type/AmountTotal.php, src/Widget/Type/AmountWeek.php, src/Widget/Type/AmountYear.php, src/Widget/Type/DailyWorkingTimeChart.php, src/Widget/Type/DurationMonth.php, src/Widget/Type/DurationToday.php, src/Widget/Type/DurationTotal.php, src/Widget/Type/DurationWeek.php, src/Widget/Type/DurationYear.php, src/Widget/Type/PaginatedWorkingTimeChart.php, src/Widget/Type/TotalsActivity.php, src/Widget/Type/TotalsCustomer.php, src/Widget/Type/TotalsProject.php, src/Widget/Type/TotalsUser.php, src/Widget/Type/UserAmountMonth.php, src/Widget/Type/UserAmountToday.php, src/Widget/Type/UserAmountTotal.php, src/Widget/Type/UserAmountWeek.php, src/Widget/Type/UserAmountYear.php, src/Widget/Type/UserDurationMonth.php, src/Widget/Type/UserDurationToday.php, src/Widget/Type/UserDurationTotal.php, src/Widget/Type/UserDurationWeek.php, src/Widget/Type/UserDurationYear.php, src/Widget/Type/UserTeamProjects.php, src/Widget/Type/UserTeams.php, src/Widget/WidgetInterface.php, src/WorkingTime/WorkingTimeService.php, symfony.lock, templates/reporting/report_by_user.html.twig, templates/reporting/report_by_user_data.html.twig, templates/reporting/report_by_user_layout.html.twig, templates/reporting/report_by_user_year.html.twig, templates/reporting/report_by_user_year_export.html.twig, templates/security/password-reset/check_email.html.twig, templates/timesheet/multi-update.html.twig, templates/user/api-token.html.twig, templates/widget/widget-counter.html.twig, templates/wizard/layout.html.twig, templates/wizard/password.html.twig, tests/API/APIControllerBaseTest.php, tests/API/UserControllerTest.php, tests/Command/UserLoginLinkCommandTest.php, tests/DependencyInjection/ConfigurationTest.php, tests/Entity/TimesheetTest.php, tests/Export/Renderer/PdfRendererTest.php, tests/Export/Timesheet/HtmlRendererTest.php, tests/Export/Timesheet/PdfRendererTest.php, tests/Invoice/Renderer/PdfRendererTest.php, tests/Invoice/Renderer/TwigRendererTest.php, tests/Invoice/templates/default.pdf.twig, tests/phpstan.neon, translations/messages.ar.xlf, translations/messages.cs.xlf, translations/messages.da.xlf, translations/messages.de.xlf, translations/messages.de_CH.xlf, translations/messages.el.xlf, translations/messages.en.xlf, translations/messages.eo.xlf, translations/messages.es.xlf, translations/messages.eu.xlf, translations/messages.fa.xlf, translations/messages.fi.xlf, translations/messages.fo.xlf, translations/messages.fr.xlf, translations/messages.he.xlf, translations/messages.hr.xlf, translations/messages.hu.xlf, translations/messages.it.xlf, translations/messages.ko.xlf, translations/messages.nb_NO.xlf, translations/messages.nl.xlf, translations/messages.pl.xlf, translations/messages.pt.xlf, translations/messages.pt_BR.xlf, translations/messages.ro.xlf, translations/messages.ru.xlf, translations/messages.sk.xlf, translations/messages.sv.xlf, translations/messages.tr.xlf, translations/messages.uk.xlf, translations/messages.vi.xlf, translations/messages.zh_CN.xlf, translations/messages.zh_Hant.xlf, translations/wizard.de.xlf, translations/wizard.en.xlf
Commit date: 2023-10-19 11:21:50 +0200
Commit message: Release 2.1.0 (#4321)

* fix deprecations
* remove unused config
* replace invalid annotation type with attribute
* use AsDoctrineListener to fix deprecation
* new ModifiedSubscriber to support custom logic and fix deprecation
* removed inheritdoc comment
* new ModifiedSubscriber to support custom logic and fix deprecation
* cleanup event dispatcher interface
* re-order annotation params
* one more doctrine based deprecation
* fix query to count active timesheets
* link to "all times" to identify active timesheets
* link icon instead of text
* fix "skin" translation in wizard
* use duration filter to show duration
* added login link command and controller
* bump tabler theme to 1.0
* added wizard to force password reset by user
* allow to configure that new accounts need to reset their password
* prevent uploading twig templates by default
* bump composer packages
* enable sandbox and basic security measures for custom twig templates for invoice and export
* bump to symfony 6.3.5
* allow to export single user reports to excel
* removed broken method to reload twig cache
* added api parameter to fetch user collection fully serialized
* allow to replace or append description via timesheet batch update
* show api username above form
Diff: diff --git a/.gitignore b/.gitignore
index 5a20c2c0..9dd9ded2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,47 +1,36 @@
 # some hosters require a htaccess to change the PHP version
-.htaccess
-.env-*
-.idea/
-.DS_Store
-rector.php
-phpstan.sh
+/public/.htaccess
+/.env-*
+/.idea/
+/.DS_Store
+/phpstan.sh
 
 # custom apache rules e.g. to deactivate ioncube loader
-public/.user.ini
+/public/.user.ini
 
 # for symfony local webserver
-php.ini
-.php-version
+/php.ini
+/.php-version
 
 # YARN 2
-.yarnrc.yml
-.yarn
+/.yarnrc.yml
+/.yarn
 
 # for keeping empty directories
-!.gitkeep
-
-/bin/*
-!/bin/console
-*local.yaml
+/config/packages/local.yaml
 /config/bundles-local.php
-
-/templates/invoice/renderer/.~lock*
-/translations/branding.*.xlf
-
 /var/data/*
-/var/coverage/
 /var/cache/*
-
-/var/invoices/*
+/var/invoices*
 /var/export/*
 /var/log/*
 /var/sessions/*
 /var/packages/*
 /var/plugins/*
 /var/plugins_old/
-*.disabled
+/var/plugins/*/*.disabled
 
 ###> symfony/framework-bundle ###
 /.env.local
 /.env.local.php
 /.env.*.local
diff --git a/composer.json b/composer.json
index d8f312b5..d924ff80 100644
--- a/composer.json
+++ b/composer.json
@@ -33,11 +33,11 @@
         "endroid/qr-code": "^4.8",
         "erusev/parsedown": "^1.6",
         "friendsofsymfony/rest-bundle": "^3.0",
         "gedmo/doctrine-extensions": "^3.6",
         "jms/serializer-bundle": "^5.0",
-        "kevinpapst/tabler-bundle": "^0.22",
+        "kevinpapst/tabler-bundle": "^1.0",
         "league/csv": "^9.4",
         "mpdf/mpdf": "^8.0",
         "nelmio/api-doc-bundle": "^4.0",
         "nelmio/cors-bundle": "^2.0",
         "onelogin/php-saml": "^4.0",
@@ -127,10 +127,11 @@
         "psr-4": {
             "App\\Tests\\": "tests/"
         }
     },
     "replace": {
+        "symfony/polyfill-ctype": "*",
         "symfony/polyfill-mbstring": "*",
         "symfony/polyfill-intl": "*",
         "symfony/polyfill-iconv": "*",
         "symfony/polyfill-php81": "*",
         "symfony/polyfill-php80": "*",
diff --git a/composer.lock b/composer.lock
index 2494b665..3aa6b4a6 100644
--- a/composer.lock
+++ b/composer.lock
@@ -2,11 +2,11 @@
     "_readme": [
         "This file locks the dependencies of your project to a known state",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "9159cc3cf1d749c412d1353e921de6fc",
+    "content-hash": "28da6d01423fca5a17395df418603aa5",
     "packages": [
         {
             "name": "azuyalabs/yasumi",
             "version": "2.6.0",
             "source": {
@@ -483,28 +483,28 @@
             ],
             "time": "2022-05-20T20:07:39+00:00"
         },
         {
             "name": "doctrine/collections",
-            "version": "2.1.3",
+            "version": "2.1.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/collections.git",
-                "reference": "3023e150f90a38843856147b58190aa8b46cc155"
+                "reference": "72328a11443a0de79967104ad36ba7b30bded134"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/collections/zipball/3023e150f90a38843856147b58190aa8b46cc155",
-                "reference": "3023e150f90a38843856147b58190aa8b46cc155",
+                "url": "https://api.github.com/repos/doctrine/collections/zipball/72328a11443a0de79967104ad36ba7b30bded134",
+                "reference": "72328a11443a0de79967104ad36ba7b30bded134",
                 "shasum": ""
             },
             "require": {
                 "doctrine/deprecations": "^1",
                 "php": "^8.1"
             },
             "require-dev": {
-                "doctrine/coding-standard": "^10.0",
+                "doctrine/coding-standard": "^12",
                 "ext-json": "*",
                 "phpstan/phpstan": "^1.8",
                 "phpstan/phpstan-phpunit": "^1.0",
                 "phpunit/phpunit": "^9.5",
                 "vimeo/psalm": "^5.11"
@@ -549,11 +549,11 @@
                 "iterators",
                 "php"
             ],
             "support": {
                 "issues": "https://github.com/doctrine/collections/issues",
-                "source": "https://github.com/doctrine/collections/tree/2.1.3"
+                "source": "https://github.com/doctrine/collections/tree/2.1.4"
             },
             "funding": [
                 {
                     "url": "https://www.doctrine-project.org/sponsorship.html",
                     "type": "custom"
@@ -565,11 +565,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-06T15:15:36+00:00"
+            "time": "2023-10-03T09:22:33+00:00"
         },
         {
             "name": "doctrine/common",
             "version": "3.4.3",
             "source": {
@@ -660,20 +660,20 @@
             ],
             "time": "2022-10-09T11:47:59+00:00"
         },
         {
             "name": "doctrine/dbal",
-            "version": "3.6.6",
+            "version": "3.7.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "63646ffd71d1676d2f747f871be31b7e921c7864"
+                "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/63646ffd71d1676d2f747f871be31b7e921c7864",
-                "reference": "63646ffd71d1676d2f747f871be31b7e921c7864",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/5b7bd66c9ff58c04c5474ab85edce442f8081cb2",
+                "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2",
                 "shasum": ""
             },
             "require": {
                 "composer-runtime-api": "^2",
                 "doctrine/cache": "^1.11|^2.0",
@@ -685,13 +685,13 @@
             },
             "require-dev": {
                 "doctrine/coding-standard": "12.0.0",
                 "fig/log-test": "^1",
                 "jetbrains/phpstorm-stubs": "2023.1",
-                "phpstan/phpstan": "1.10.29",
+                "phpstan/phpstan": "1.10.35",
                 "phpstan/phpstan-strict-rules": "^1.5",
-                "phpunit/phpunit": "9.6.9",
+                "phpunit/phpunit": "9.6.13",
                 "psalm/plugin-phpunit": "0.18.4",
                 "slevomat/coding-standard": "8.13.1",
                 "squizlabs/php_codesniffer": "3.7.2",
                 "symfony/cache": "^5.4|^6.0",
                 "symfony/console": "^4.4|^5.4|^6.0",
@@ -753,11 +753,11 @@
                 "sqlserver",
                 "sqlsrv"
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.6.6"
+                "source": "https://github.com/doctrine/dbal/tree/3.7.1"
             },
             "funding": [
                 {
                     "url": "https://www.doctrine-project.org/sponsorship.html",
                     "type": "custom"
@@ -769,24 +769,24 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-17T05:38:17+00:00"
+            "time": "2023-10-06T05:06:20+00:00"
         },
         {
             "name": "doctrine/deprecations",
-            "version": "v1.1.1",
+            "version": "1.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/deprecations.git",
-                "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3"
+                "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
-                "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
+                "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931",
+                "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.1 || ^8.0"
             },
@@ -814,13 +814,13 @@
             ],
             "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
             "homepage": "https://www.doctrine-project.org/",
             "support": {
                 "issues": "https://github.com/doctrine/deprecations/issues",
-                "source": "https://github.com/doctrine/deprecations/tree/v1.1.1"
+                "source": "https://github.com/doctrine/deprecations/tree/1.1.2"
             },
-            "time": "2023-06-03T09:27:29+00:00"
+            "time": "2023-09-27T20:04:15+00:00"
         },
         {
             "name": "doctrine/doctrine-bundle",
             "version": "2.10.2",
             "source": {
@@ -1704,30 +1704,30 @@
             },
             "time": "2022-05-23T21:33:49+00:00"
         },
         {
             "name": "egulias/email-validator",
-            "version": "4.0.1",
+            "version": "4.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/egulias/EmailValidator.git",
-                "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff"
+                "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/3a85486b709bc384dae8eb78fb2eec649bdb64ff",
-                "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e",
+                "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e",
                 "shasum": ""
             },
             "require": {
                 "doctrine/lexer": "^2.0 || ^3.0",
                 "php": ">=8.1",
                 "symfony/polyfill-intl-idn": "^1.26"
             },
             "require-dev": {
-                "phpunit/phpunit": "^9.5.27",
-                "vimeo/psalm": "^4.30"
+                "phpunit/phpunit": "^10.2",
+                "vimeo/psalm": "^5.12"
             },
             "suggest": {
                 "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
             },
             "type": "library",
@@ -1759,32 +1759,32 @@
                 "validation",
                 "validator"
             ],
             "support": {
                 "issues": "https://github.com/egulias/EmailValidator/issues",
-                "source": "https://github.com/egulias/EmailValidator/tree/4.0.1"
+                "source": "https://github.com/egulias/EmailValidator/tree/4.0.2"
             },
             "funding": [
                 {
                     "url": "https://github.com/egulias",
                     "type": "github"
                 }
             ],
-            "time": "2023-01-14T14:17:03+00:00"
+            "time": "2023-10-06T06:47:41+00:00"
         },
         {
             "name": "endroid/qr-code",
-            "version": "4.8.4",
+            "version": "4.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/endroid/qr-code.git",
-                "reference": "a122b85d4a5a3257d471257a43ac3e5676a27ffe"
+                "reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/endroid/qr-code/zipball/a122b85d4a5a3257d471257a43ac3e5676a27ffe",
-                "reference": "a122b85d4a5a3257d471257a43ac3e5676a27ffe",
+                "url": "https://api.github.com/repos/endroid/qr-code/zipball/0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
+                "reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
                 "shasum": ""
             },
             "require": {
                 "bacon/bacon-qr-code": "^2.0.5",
                 "php": "^8.1"
@@ -1834,19 +1834,19 @@
                 "qr",
                 "qrcode"
             ],
             "support": {
                 "issues": "https://github.com/endroid/qr-code/issues",
-                "source": "https://github.com/endroid/qr-code/tree/4.8.4"
+                "source": "https://github.com/endroid/qr-code/tree/4.8.5"
             },
             "funding": [
                 {
                     "url": "https://github.com/endroid",
                     "type": "github"
                 }
             ],
-            "time": "2023-08-28T18:12:07+00:00"
+            "time": "2023-09-29T14:03:20+00:00"
         },
         {
             "name": "erusev/parsedown",
             "version": "1.7.4",
             "source": {
@@ -1957,63 +1957,62 @@
             },
             "time": "2022-09-18T07:06:19+00:00"
         },
         {
             "name": "friendsofsymfony/rest-bundle",
-            "version": "3.5.0",
+            "version": "3.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfSymfony/FOSRestBundle.git",
-                "reference": "893f3a01e4d88789abc399c6f1b3cfff79238734"
+                "reference": "e01be8113d4451adb3cbb29d7d2cc96bbc698179"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/893f3a01e4d88789abc399c6f1b3cfff79238734",
-                "reference": "893f3a01e4d88789abc399c6f1b3cfff79238734",
+                "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/e01be8113d4451adb3cbb29d7d2cc96bbc698179",
+                "reference": "e01be8113d4451adb3cbb29d7d2cc96bbc698179",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.2|^8.0",
-                "symfony/config": "^4.4|^5.3|^6.0",
-                "symfony/dependency-injection": "^4.4|^5.3|^6.0",
-                "symfony/event-dispatcher": "^4.4|^5.3|^6.0",
+                "symfony/config": "^5.4|^6.0",
+                "symfony/dependency-injection": "^5.4|^6.0",
+                "symfony/event-dispatcher": "^5.4|^6.0",
                 "symfony/framework-bundle": "^4.4.1|^5.0|^6.0",
-                "symfony/http-foundation": "^4.4|^5.3|^6.0",
-                "symfony/http-kernel": "^4.4|^5.3|^6.0",
-                "symfony/routing": "^4.4|^5.3|^6.0",
-                "symfony/security-core": "^4.4|^5.3|^6.0",
+                "symfony/http-foundation": "^5.4|^6.0",
+                "symfony/http-kernel": "^5.4|^6.0",
+                "symfony/routing": "^5.4|^6.0",
+                "symfony/security-core": "^5.4|^6.0",
                 "willdurand/jsonp-callback-validator": "^1.0|^2.0",
                 "willdurand/negotiation": "^2.0|^3.0"
             },
             "conflict": {
                 "doctrine/annotations": "<1.12",
                 "jms/serializer": "<1.13.0",
                 "jms/serializer-bundle": "<2.4.3|3.0.0",
-                "sensio/framework-extra-bundle": "<6.1",
-                "symfony/error-handler": "<4.4.1"
+                "sensio/framework-extra-bundle": "<6.1"
             },
             "require-dev": {
-                "doctrine/annotations": "^1.13.2",
+                "doctrine/annotations": "^1.13.2|^2.0 ",
                 "friendsofphp/php-cs-fixer": "^3.0",
                 "jms/serializer": "^1.13|^2.0|^3.0",
                 "jms/serializer-bundle": "^2.4.3|^3.0.1|^4.0|^5.0",
                 "psr/http-message": "^1.0",
                 "psr/log": "^1.0|^2.0|^3.0",
                 "sensio/framework-extra-bundle": "^6.1",
-                "symfony/asset": "^4.4|^5.3|^6.0",
-                "symfony/browser-kit": "^4.4|^5.3|^6.0",
-                "symfony/css-selector": "^4.4|^5.3|^6.0",
-                "symfony/expression-language": "^4.4|^5.3|^6.0",
-                "symfony/form": "^4.4|^5.3|^6.0",
-                "symfony/mime": "^4.4|^5.3|^6.0",
-                "symfony/phpunit-bridge": "^5.3|^6.0",
-                "symfony/security-bundle": "^4.4|^5.3|^6.0",
-                "symfony/serializer": "^4.4|^5.3|^6.0",
-                "symfony/twig-bundle": "^4.4|^5.3|^6.0",
-                "symfony/validator": "^4.4|^5.3|^6.0",
-                "symfony/web-profiler-bundle": "^4.4|^5.3|^6.0",
-                "symfony/yaml": "^4.4|^5.3|^6.0"
+                "symfony/asset": "^5.4|^6.0",
+                "symfony/browser-kit": "^5.4|^6.0",
+                "symfony/css-selector": "^5.4|^6.0",
+                "symfony/expression-language": "^5.4|^6.0",
+                "symfony/form": "^5.4|^6.0",
+                "symfony/mime": "^5.4|^6.0",
+                "symfony/phpunit-bridge": "^5.4|^6.0",
+                "symfony/security-bundle": "^5.4|^6.0",
+                "symfony/serializer": "^5.4|^6.0",
+                "symfony/twig-bundle": "^5.4|^6.0",
+                "symfony/validator": "^5.4|^6.0",
+                "symfony/web-profiler-bundle": "^5.4|^6.0",
+                "symfony/yaml": "^5.4|^6.0"
             },
             "suggest": {
                 "jms/serializer-bundle": "Add support for advanced serialization capabilities, recommended, requires ^2.0|^3.0",
                 "sensio/framework-extra-bundle": "Add support for the request body converter and the view response listener, requires ^3.0",
                 "symfony/serializer": "Add support for basic serialization capabilities and xml decoding, requires ^2.7|^3.0",
@@ -2057,13 +2056,13 @@
             "keywords": [
                 "rest"
             ],
             "support": {
                 "issues": "https://github.com/FriendsOfSymfony/FOSRestBundle/issues",
-                "source": "https://github.com/FriendsOfSymfony/FOSRestBundle/tree/3.5.0"
+                "source": "https://github.com/FriendsOfSymfony/FOSRestBundle/tree/3.6.0"
             },
-            "time": "2023-01-06T15:33:47+00:00"
+            "time": "2023-09-27T11:41:02+00:00"
         },
         {
             "name": "gedmo/doctrine-extensions",
             "version": "v3.13.0",
             "source": {
@@ -2437,45 +2436,44 @@
             ],
             "time": "2023-06-13T14:47:57+00:00"
         },
         {
             "name": "kevinpapst/tabler-bundle",
-            "version": "0.22",
+            "version": "1.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/kevinpapst/TablerBundle.git",
-                "reference": "fdc3c182f13dec439ce5110dededba2895518f8f"
+                "reference": "9554efc6f96d757decbc4b52402eb964e9c90786"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/fdc3c182f13dec439ce5110dededba2895518f8f",
-                "reference": "fdc3c182f13dec439ce5110dededba2895518f8f",
+                "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/9554efc6f96d757decbc4b52402eb964e9c90786",
+                "reference": "9554efc6f96d757decbc4b52402eb964e9c90786",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.4",
-                "symfony/asset": "^4.4 || ^5.4 || ^6.0",
-                "symfony/config": "^4.4 || ^5.4 || ^6.0",
-                "symfony/dependency-injection": "^4.4 || ^5.4 || ^6.0",
-                "symfony/event-dispatcher": "^4.4 || ^5.4 || ^6.0",
-                "symfony/http-foundation": "^4.4 || ^5.4 || ^6.0",
-                "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0",
-                "symfony/options-resolver": "^4.4 || ^5.4 || ^6.0",
-                "symfony/security-core": "^4.4 || ^5.4 || ^6.0",
-                "symfony/translation": "^4.4 || ^5.4 || ^6.0",
-                "symfony/twig-bridge": "^4.4 || ^5.4 || ^6.0",
+                "php": "8.1.*||8.2.*",
+                "symfony/asset": "^6.0",
+                "symfony/config": "^6.0",
+                "symfony/dependency-injection": "^6.0",
+                "symfony/event-dispatcher": "^6.0",
+                "symfony/http-foundation": "^6.0",
+                "symfony/http-kernel": "^6.0",
+                "symfony/options-resolver": "^6.0",
+                "symfony/security-core": "^6.0",
+                "symfony/translation": "^6.0",
+                "symfony/twig-bridge": "^6.0",
                 "twig/twig": "^3.0"
             },
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "^3.0",
                 "knplabs/knp-menu-bundle": "^3.0",
-                "phpspec/prophecy": "^1.6",
                 "phpstan/phpstan": "^1.0",
                 "phpstan/phpstan-phpunit": "^1.0",
                 "phpstan/phpstan-symfony": "^1.0",
                 "phpunit/phpunit": "^9.0",
-                "symfony/framework-bundle": "^4.4 || ^5.4 || ^6.0"
+                "symfony/framework-bundle": "^6.0"
             },
             "suggest": {
                 "knplabs/knp-menu-bundle": "Allows easy menu integration"
             },
             "type": "symfony-bundle",
@@ -2495,11 +2493,11 @@
                 }
             ],
             "description": "Admin/Backend theme bundle for Symfony based on Tabler.io",
             "support": {
                 "issues": "https://github.com/kevinpapst/TablerBundle/issues",
-                "source": "https://github.com/kevinpapst/TablerBundle/tree/0.22"
+                "source": "https://github.com/kevinpapst/TablerBundle/tree/1.0.0"
             },
             "funding": [
                 {
                     "url": "https://paypal.me/kevinpapst",
                     "type": "custom"
@@ -2507,41 +2505,41 @@
                 {
                     "url": "https://github.com/kevinpapst",
                     "type": "github"
                 }
             ],
-            "time": "2023-06-15T06:00:11+00:00"
+            "time": "2023-09-27T13:27:36+00:00"
         },
         {
             "name": "laminas/laminas-escaper",
-            "version": "2.12.0",
+            "version": "2.13.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laminas/laminas-escaper.git",
-                "reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490"
+                "reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490",
-                "reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490",
+                "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/af459883f4018d0f8a0c69c7a209daef3bf973ba",
+                "reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba",
                 "shasum": ""
             },
             "require": {
                 "ext-ctype": "*",
                 "ext-mbstring": "*",
-                "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0"
+                "php": "~8.1.0 || ~8.2.0 || ~8.3.0"
             },
             "conflict": {
                 "zendframework/zend-escaper": "*"
             },
             "require-dev": {
-                "infection/infection": "^0.26.6",
-                "laminas/laminas-coding-standard": "~2.4.0",
+                "infection/infection": "^0.27.0",
+                "laminas/laminas-coding-standard": "~2.5.0",
                 "maglnet/composer-require-checker": "^3.8.0",
-                "phpunit/phpunit": "^9.5.18",
-                "psalm/plugin-phpunit": "^0.17.0",
-                "vimeo/psalm": "^4.22.0"
+                "phpunit/phpunit": "^9.6.7",
+                "psalm/plugin-phpunit": "^0.18.4",
+                "vimeo/psalm": "^5.9"
             },
             "type": "library",
             "autoload": {
                 "psr-4": {
                     "Laminas\\Escaper\\": "src/"
@@ -2569,24 +2567,24 @@
                 {
                     "url": "https://funding.communitybridge.org/projects/laminas-project",
                     "type": "community_bridge"
                 }
             ],
-            "time": "2022-10-10T10:11:09+00:00"
+            "time": "2023-10-10T08:35:13+00:00"
         },
         {
             "name": "league/csv",
-            "version": "9.10.0",
+            "version": "9.11.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/csv.git",
-                "reference": "d24b0d484812313b07ab74b0fe4db9661606df6c"
+                "reference": "33149c4bea4949aa4fa3d03fb11ed28682168b39"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/csv/zipball/d24b0d484812313b07ab74b0fe4db9661606df6c",
-                "reference": "d24b0d484812313b07ab74b0fe4db9661606df6c",
+                "url": "https://api.github.com/repos/thephpleague/csv/zipball/33149c4bea4949aa4fa3d03fb11ed28682168b39",
+                "reference": "33149c4bea4949aa4fa3d03fb11ed28682168b39",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "ext-mbstring": "*",
@@ -2657,11 +2655,11 @@
                 {
                     "url": "https://github.com/sponsors/nyamsprod",
                     "type": "github"
                 }
             ],
-            "time": "2023-08-04T15:12:48+00:00"
+            "time": "2023-09-23T10:09:54+00:00"
         },
         {
             "name": "lorenzo/pinky",
             "version": "1.1.0",
             "source": {
@@ -3084,20 +3082,20 @@
             ],
             "time": "2023-09-01T11:44:52+00:00"
         },
         {
             "name": "mpdf/psr-http-message-shim",
-            "version": "2.0.0",
+            "version": "v2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/mpdf/psr-http-message-shim.git",
-                "reference": "1cf4c0b68b8461cea27411ff961482ce7687e34f"
+                "reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/1cf4c0b68b8461cea27411ff961482ce7687e34f",
-                "reference": "1cf4c0b68b8461cea27411ff961482ce7687e34f",
+                "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
+                "reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
                 "shasum": ""
             },
             "require": {
                 "psr/http-message": "^2.0"
             },
@@ -3126,13 +3124,13 @@
                 }
             ],
             "description": "Shim to allow support of different psr/message versions.",
             "support": {
                 "issues": "https://github.com/mpdf/psr-http-message-shim/issues",
-                "source": "https://github.com/mpdf/psr-http-message-shim/tree/2.0.0"
+                "source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
             },
-            "time": "2023-09-01T06:08:18+00:00"
+            "time": "2023-10-02T14:34:03+00:00"
         },
         {
             "name": "mpdf/psr-log-aware-trait",
             "version": "v3.0.0",
             "source": {
@@ -4066,20 +4064,20 @@
             },
             "time": "2023-05-30T07:59:14+00:00"
         },
         {
             "name": "phpstan/phpdoc-parser",
-            "version": "1.24.1",
+            "version": "1.24.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpdoc-parser.git",
-                "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01"
+                "reference": "bcad8d995980440892759db0c32acae7c8e79442"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01",
-                "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01",
+                "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442",
+                "reference": "bcad8d995980440892759db0c32acae7c8e79442",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.2 || ^8.0"
             },
@@ -4107,13 +4105,13 @@
                 "MIT"
             ],
             "description": "PHPDoc parser with support for nullable, intersection and generic types",
             "support": {
                 "issues": "https://github.com/phpstan/phpdoc-parser/issues",
-                "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.1"
+                "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.2"
             },
-            "time": "2023-09-18T12:18:02+00:00"
+            "time": "2023-09-26T12:28:12+00:00"
         },
         {
             "name": "psr/cache",
             "version": "3.0.0",
             "source": {
@@ -4313,20 +4311,20 @@
             },
             "time": "2019-01-08T18:20:26+00:00"
         },
         {
             "name": "psr/http-client",
-            "version": "1.0.2",
+            "version": "1.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-fig/http-client.git",
-                "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31"
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31",
-                "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31",
+                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0 || ^8.0",
                 "psr/http-message": "^1.0 || ^2.0"
@@ -4359,13 +4357,13 @@
                 "http-client",
                 "psr",
                 "psr-18"
             ],
             "support": {
-                "source": "https://github.com/php-fig/http-client/tree/1.0.2"
+                "source": "https://github.com/php-fig/http-client"
             },
-            "time": "2023-04-10T20:12:12+00:00"
+            "time": "2023-09-23T14:17:50+00:00"
         },
         {
             "name": "psr/http-factory",
             "version": "1.0.2",
             "source": {
@@ -4783,20 +4781,20 @@
             },
             "time": "2022-12-10T15:20:09+00:00"
         },
         {
             "name": "setasign/fpdi",
-            "version": "v2.4.1",
+            "version": "v2.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Setasign/FPDI.git",
-                "reference": "f4ba73e5bc053ccc90b81717c5df1cb2ea7bae7b"
+                "reference": "ecf0459643ec963febfb9a5d529dcd93656006a4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Setasign/FPDI/zipball/f4ba73e5bc053ccc90b81717c5df1cb2ea7bae7b",
-                "reference": "f4ba73e5bc053ccc90b81717c5df1cb2ea7bae7b",
+                "url": "https://api.github.com/repos/Setasign/FPDI/zipball/ecf0459643ec963febfb9a5d529dcd93656006a4",
+                "reference": "ecf0459643ec963febfb9a5d529dcd93656006a4",
                 "shasum": ""
             },
             "require": {
                 "ext-zlib": "*",
                 "php": "^5.6 || ^7.0 || ^8.0"
@@ -4843,19 +4841,19 @@
                 "fpdi",
                 "pdf"
             ],
             "support": {
                 "issues": "https://github.com/Setasign/FPDI/issues",
-                "source": "https://github.com/Setasign/FPDI/tree/v2.4.1"
+                "source": "https://github.com/Setasign/FPDI/tree/v2.5.0"
             },
             "funding": [
                 {
                     "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-27T08:12:09+00:00"
+            "time": "2023-09-28T10:46:27+00:00"
         },
         {
             "name": "spomky-labs/otphp",
             "version": "11.2.0",
             "source": {
@@ -5004,20 +5002,20 @@
             ],
             "time": "2023-04-21T14:41:17+00:00"
         },
         {
             "name": "symfony/cache",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/cache.git",
-                "reference": "e60d00b4f633efa4c1ef54e77c12762d9073e7b3"
+                "reference": "6c1a3ea078c4d88ee892530945df63a87981b2da"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/cache/zipball/e60d00b4f633efa4c1ef54e77c12762d9073e7b3",
-                "reference": "e60d00b4f633efa4c1ef54e77c12762d9073e7b3",
+                "url": "https://api.github.com/repos/symfony/cache/zipball/6c1a3ea078c4d88ee892530945df63a87981b2da",
+                "reference": "6c1a3ea078c4d88ee892530945df63a87981b2da",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "psr/cache": "^2.0|^3.0",
@@ -5080,11 +5078,11 @@
             "keywords": [
                 "caching",
                 "psr6"
             ],
             "support": {
-                "source": "https://github.com/symfony/cache/tree/v6.3.4"
+                "source": "https://github.com/symfony/cache/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -5096,11 +5094,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-05T09:10:27+00:00"
+            "time": "2023-09-26T15:48:55+00:00"
         },
         {
             "name": "symfony/cache-contracts",
             "version": "v3.3.0",
             "source": {
@@ -5479,20 +5477,20 @@
             ],
             "time": "2023-07-12T16:00:22+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "68a5a9570806a087982f383f6109c5e925892a49"
+                "reference": "2ed62b3bf98346e1f45529a7b6be2196739bb993"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/68a5a9570806a087982f383f6109c5e925892a49",
-                "reference": "68a5a9570806a087982f383f6109c5e925892a49",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2ed62b3bf98346e1f45529a7b6be2196739bb993",
+                "reference": "2ed62b3bf98346e1f45529a7b6be2196739bb993",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "psr/container": "^1.1|^2.0",
@@ -5540,11 +5538,11 @@
                 }
             ],
             "description": "Allows you to standardize and centralize the way objects are constructed in your application",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/dependency-injection/tree/v6.3.4"
+                "source": "https://github.com/symfony/dependency-injection/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -5556,11 +5554,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-16T17:55:17+00:00"
+            "time": "2023-09-25T16:46:40+00:00"
         },
         {
             "name": "symfony/deprecation-contracts",
             "version": "v3.3.0",
             "source": {
@@ -5627,20 +5625,20 @@
             ],
             "time": "2023-05-23T14:45:45+00:00"
         },
         {
             "name": "symfony/doctrine-bridge",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/doctrine-bridge.git",
-                "reference": "589eeeb93669739ec1d8bd4593e4972d94e0981d"
+                "reference": "9977eb1adf999ceded213e88c1ac6dff7a1a0306"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/589eeeb93669739ec1d8bd4593e4972d94e0981d",
-                "reference": "589eeeb93669739ec1d8bd4593e4972d94e0981d",
+                "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/9977eb1adf999ceded213e88c1ac6dff7a1a0306",
+                "reference": "9977eb1adf999ceded213e88c1ac6dff7a1a0306",
                 "shasum": ""
             },
             "require": {
                 "doctrine/event-manager": "^1.2|^2",
                 "doctrine/persistence": "^2|^3",
@@ -5717,11 +5715,11 @@
                 }
             ],
             "description": "Provides integration for Doctrine with various Symfony components",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/doctrine-bridge/tree/v6.3.4"
+                "source": "https://github.com/symfony/doctrine-bridge/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -5733,11 +5731,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-08T10:40:25+00:00"
+            "time": "2023-09-29T16:16:03+00:00"
         },
         {
             "name": "symfony/dotenv",
             "version": "v6.3.0",
             "source": {
@@ -5811,20 +5809,20 @@
             ],
             "time": "2023-04-21T14:41:17+00:00"
         },
         {
             "name": "symfony/error-handler",
-            "version": "v6.3.2",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/error-handler.git",
-                "reference": "85fd65ed295c4078367c784e8a5a6cee30348b7a"
+                "reference": "1f69476b64fb47105c06beef757766c376b548c4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/error-handler/zipball/85fd65ed295c4078367c784e8a5a6cee30348b7a",
-                "reference": "85fd65ed295c4078367c784e8a5a6cee30348b7a",
+                "url": "https://api.github.com/repos/symfony/error-handler/zipball/1f69476b64fb47105c06beef757766c376b548c4",
+                "reference": "1f69476b64fb47105c06beef757766c376b548c4",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "psr/log": "^1|^2|^3",
@@ -5865,11 +5863,11 @@
                 }
             ],
             "description": "Provides tools to manage errors and ease debugging PHP code",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/error-handler/tree/v6.3.2"
+                "source": "https://github.com/symfony/error-handler/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -5881,11 +5879,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-16T17:05:46+00:00"
+            "time": "2023-09-12T06:57:20+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
             "version": "v6.3.2",
             "source": {
@@ -6168,20 +6166,20 @@
             ],
             "time": "2023-06-01T08:30:39+00:00"
         },
         {
             "name": "symfony/finder",
-            "version": "v6.3.3",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e"
+                "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/9915db259f67d21eefee768c1abcf1cc61b1fc9e",
-                "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/a1b31d88c0e998168ca7792f222cbecee47428c4",
+                "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1"
             },
@@ -6212,11 +6210,11 @@
                 }
             ],
             "description": "Finds files and directories via an intuitive fluent interface",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/finder/tree/v6.3.3"
+                "source": "https://github.com/symfony/finder/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -6228,11 +6226,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-31T08:31:44+00:00"
+            "time": "2023-09-26T12:56:25+00:00"
         },
         {
             "name": "symfony/flex",
             "version": "v2.3.3",
             "source": {
@@ -6297,20 +6295,20 @@
             ],
             "time": "2023-08-04T09:02:35+00:00"
         },
         {
             "name": "symfony/form",
-            "version": "v6.3.2",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/form.git",
-                "reference": "afdadf511e08bc6d4752afb869ce084276aca4e2"
+                "reference": "0f9ad8600c1021983d096512066ee54332aa3139"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/form/zipball/afdadf511e08bc6d4752afb869ce084276aca4e2",
-                "reference": "afdadf511e08bc6d4752afb869ce084276aca4e2",
+                "url": "https://api.github.com/repos/symfony/form/zipball/0f9ad8600c1021983d096512066ee54332aa3139",
+                "reference": "0f9ad8600c1021983d096512066ee54332aa3139",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/deprecation-contracts": "^2.5|^3",
@@ -6374,11 +6372,11 @@
                 }
             ],
             "description": "Allows to easily create, process and reuse HTML forms",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/form/tree/v6.3.2"
+                "source": "https://github.com/symfony/form/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -6390,24 +6388,24 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-26T17:39:03+00:00"
+            "time": "2023-09-10T17:47:23+00:00"
         },
         {
             "name": "symfony/framework-bundle",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/framework-bundle.git",
-                "reference": "f822f54ff05cd88878910b4559f66c12176d952c"
+                "reference": "567cafcfc08e3076b47290a7558b0ca17a98b0ce"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/f822f54ff05cd88878910b4559f66c12176d952c",
-                "reference": "f822f54ff05cd88878910b4559f66c12176d952c",
+                "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/567cafcfc08e3076b47290a7558b0ca17a98b0ce",
+                "reference": "567cafcfc08e3076b47290a7558b0ca17a98b0ce",
                 "shasum": ""
             },
             "require": {
                 "composer-runtime-api": ">=2.1",
                 "ext-xml": "*",
@@ -6518,11 +6516,11 @@
                 }
             ],
             "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/framework-bundle/tree/v6.3.4"
+                "source": "https://github.com/symfony/framework-bundle/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -6534,24 +6532,24 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-16T18:04:38+00:00"
+            "time": "2023-09-29T10:45:15+00:00"
         },
         {
             "name": "symfony/http-client",
-            "version": "v6.3.2",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-client.git",
-                "reference": "15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00"
+                "reference": "213e564da4cbf61acc9728d97e666bcdb868c10d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-client/zipball/15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00",
-                "reference": "15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00",
+                "url": "https://api.github.com/repos/symfony/http-client/zipball/213e564da4cbf61acc9728d97e666bcdb868c10d",
+                "reference": "213e564da4cbf61acc9728d97e666bcdb868c10d",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "psr/log": "^1|^2|^3",
@@ -6610,11 +6608,11 @@
             "homepage": "https://symfony.com",
             "keywords": [
                 "http"
             ],
             "support": {
-                "source": "https://github.com/symfony/http-client/tree/v6.3.2"
+                "source": "https://github.com/symfony/http-client/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -6626,11 +6624,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-05T08:41:27+00:00"
+            "time": "2023-09-29T15:57:12+00:00"
         },
         {
             "name": "symfony/http-client-contracts",
             "version": "v3.3.0",
             "source": {
@@ -6708,20 +6706,20 @@
             ],
             "time": "2023-05-23T14:45:45+00:00"
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "cac1556fdfdf6719668181974104e6fcfa60e844"
+                "reference": "b50f5e281d722cb0f4c296f908bacc3e2b721957"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/cac1556fdfdf6719668181974104e6fcfa60e844",
-                "reference": "cac1556fdfdf6719668181974104e6fcfa60e844",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b50f5e281d722cb0f4c296f908bacc3e2b721957",
+                "reference": "b50f5e281d722cb0f4c296f908bacc3e2b721957",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/deprecation-contracts": "^2.5|^3",
@@ -6765,11 +6763,11 @@
                 }
             ],
             "description": "Defines an object-oriented layer for the HTTP specification",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-foundation/tree/v6.3.4"
+                "source": "https://github.com/symfony/http-foundation/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -6781,24 +6779,24 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-22T08:20:46+00:00"
+            "time": "2023-09-04T21:33:54+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb"
+                "reference": "9f991a964368bee8d883e8d57ced4fe9fff04dfc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb",
-                "reference": "36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f991a964368bee8d883e8d57ced4fe9fff04dfc",
+                "reference": "9f991a964368bee8d883e8d57ced4fe9fff04dfc",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "psr/log": "^1|^2|^3",
@@ -6878,11 +6876,11 @@
                 }
             ],
             "description": "Provides a structured process for converting a Request into a Response",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-kernel/tree/v6.3.4"
+                "source": "https://github.com/symfony/http-kernel/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -6894,11 +6892,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-26T13:54:49+00:00"
+            "time": "2023-09-30T06:37:04+00:00"
         },
         {
             "name": "symfony/intl",
             "version": "v6.3.2",
             "source": {
@@ -6980,20 +6978,20 @@
             ],
             "time": "2023-07-20T07:43:09+00:00"
         },
         {
             "name": "symfony/mailer",
-            "version": "v6.3.0",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mailer.git",
-                "reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435"
+                "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mailer/zipball/7b03d9be1dea29bfec0a6c7b603f5072a4c97435",
-                "reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435",
+                "url": "https://api.github.com/repos/symfony/mailer/zipball/d89611a7830d51b5e118bca38e390dea92f9ea06",
+                "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06",
                 "shasum": ""
             },
             "require": {
                 "egulias/email-validator": "^2.1.10|^3|^4",
                 "php": ">=8.1",
@@ -7040,11 +7038,11 @@
                 }
             ],
             "description": "Helps sending emails",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/mailer/tree/v6.3.0"
+                "source": "https://github.com/symfony/mailer/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -7056,24 +7054,24 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-05-29T12:49:39+00:00"
+            "time": "2023-09-06T09:47:15+00:00"
         },
         {
             "name": "symfony/mime",
-            "version": "v6.3.3",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-                "reference": "9a0cbd52baa5ba5a5b1f0cacc59466f194730f98"
+                "reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mime/zipball/9a0cbd52baa5ba5a5b1f0cacc59466f194730f98",
-                "reference": "9a0cbd52baa5ba5a5b1f0cacc59466f194730f98",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/d5179eedf1cb2946dbd760475ebf05c251ef6a6e",
+                "reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/deprecation-contracts": "^2.5|^3",
@@ -7124,11 +7122,11 @@
             "keywords": [
                 "mime",
                 "mime-type"
             ],
             "support": {
-                "source": "https://github.com/symfony/mime/tree/v6.3.3"
+                "source": "https://github.com/symfony/mime/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -7140,11 +7138,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-31T07:08:24+00:00"
+            "time": "2023-09-29T06:59:36+00:00"
         },
         {
             "name": "symfony/monolog-bridge",
             "version": "v6.3.1",
             "source": {
@@ -7370,20 +7368,20 @@
             ],
             "time": "2023-05-12T14:21:09+00:00"
         },
         {
             "name": "symfony/password-hasher",
-            "version": "v6.3.0",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/password-hasher.git",
-                "reference": "d23ad221989e6b8278d050cabfd7b569eee84590"
+                "reference": "278d3a49715073879f75e372ad80b8cfeca949d3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/password-hasher/zipball/d23ad221989e6b8278d050cabfd7b569eee84590",
-                "reference": "d23ad221989e6b8278d050cabfd7b569eee84590",
+                "url": "https://api.github.com/repos/symfony/password-hasher/zipball/278d3a49715073879f75e372ad80b8cfeca949d3",
+                "reference": "278d3a49715073879f75e372ad80b8cfeca949d3",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1"
             },
@@ -7422,11 +7420,11 @@
             "keywords": [
                 "hashing",
                 "password"
             ],
             "support": {
-                "source": "https://github.com/symfony/password-hasher/tree/v6.3.0"
+                "source": "https://github.com/symfony/password-hasher/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -7438,93 +7436,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-02-14T09:04:20+00:00"
-        },
-        {
-            "name": "symfony/polyfill-ctype",
-            "version": "v1.28.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
-                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "provide": {
-                "ext-ctype": "*"
-            },
-            "suggest": {
-                "ext-ctype": "For best performance"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.28-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "files": [
-                    "bootstrap.php"
-                ],
-                "psr-4": {
-                    "Symfony\\Polyfill\\Ctype\\": ""
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Gert de Pagter",
-                    "email": "BackEndTea@gmail.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill for ctype functions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "ctype",
-                "polyfill",
-                "portable"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.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": "2023-01-26T09:26:14+00:00"
+            "time": "2023-09-25T17:05:16+00:00"
         },
         {
             "name": "symfony/polyfill-intl-grapheme",
             "version": "v1.28.0",
             "source": {
@@ -8173,20 +8089,20 @@
             ],
             "time": "2023-07-13T14:29:38+00:00"
         },
         {
             "name": "symfony/routing",
-            "version": "v6.3.3",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/routing.git",
-                "reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a"
+                "reference": "82616e59acd3e3d9c916bba798326cb7796d7d31"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/routing/zipball/e7243039ab663822ff134fbc46099b5fdfa16f6a",
-                "reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a",
+                "url": "https://api.github.com/repos/symfony/routing/zipball/82616e59acd3e3d9c916bba798326cb7796d7d31",
+                "reference": "82616e59acd3e3d9c916bba798326cb7796d7d31",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/deprecation-contracts": "^2.5|^3"
@@ -8236,11 +8152,11 @@
                 "routing",
                 "uri",
                 "url"
             ],
             "support": {
-                "source": "https://github.com/symfony/routing/tree/v6.3.3"
+                "source": "https://github.com/symfony/routing/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -8252,11 +8168,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-31T07:08:24+00:00"
+            "time": "2023-09-20T16:05:51+00:00"
         },
         {
             "name": "symfony/runtime",
             "version": "v6.3.2",
             "source": {
@@ -8335,20 +8251,20 @@
             ],
             "time": "2023-07-16T17:05:46+00:00"
         },
         {
             "name": "symfony/security-bundle",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/security-bundle.git",
-                "reference": "31957477b289220a47880ead3727bf5cc059fa08"
+                "reference": "2df460eacceb11b9287cfafddda4d27023dd9001"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/security-bundle/zipball/31957477b289220a47880ead3727bf5cc059fa08",
-                "reference": "31957477b289220a47880ead3727bf5cc059fa08",
+                "url": "https://api.github.com/repos/symfony/security-bundle/zipball/2df460eacceb11b9287cfafddda4d27023dd9001",
+                "reference": "2df460eacceb11b9287cfafddda4d27023dd9001",
                 "shasum": ""
             },
             "require": {
                 "composer-runtime-api": ">=2.1",
                 "ext-xml": "*",
@@ -8361,11 +8277,11 @@
                 "symfony/http-foundation": "^6.2",
                 "symfony/http-kernel": "^6.2",
                 "symfony/password-hasher": "^5.4|^6.0",
                 "symfony/security-core": "^6.2",
                 "symfony/security-csrf": "^5.4|^6.0",
-                "symfony/security-http": "^6.3"
+                "symfony/security-http": "^6.3.4"
             },
             "conflict": {
                 "symfony/browser-kit": "<5.4",
                 "symfony/console": "<5.4",
                 "symfony/framework-bundle": "<6.3",
@@ -8425,11 +8341,11 @@
                 }
             ],
             "description": "Provides a tight integration of the Security component into the Symfony full-stack framework",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/security-bundle/tree/v6.3.4"
+                "source": "https://github.com/symfony/security-bundle/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -8441,24 +8357,24 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-25T08:46:23+00:00"
+            "time": "2023-09-25T17:05:55+00:00"
         },
         {
             "name": "symfony/security-core",
-            "version": "v6.3.3",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/security-core.git",
-                "reference": "b86ce012cc9a62a15ed43af5037eebc3e6de4d7f"
+                "reference": "ec8f24dc1195f46483510892271d01a5202bba70"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/security-core/zipball/b86ce012cc9a62a15ed43af5037eebc3e6de4d7f",
-                "reference": "b86ce012cc9a62a15ed43af5037eebc3e6de4d7f",
+                "url": "https://api.github.com/repos/symfony/security-core/zipball/ec8f24dc1195f46483510892271d01a5202bba70",
+                "reference": "ec8f24dc1195f46483510892271d01a5202bba70",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/deprecation-contracts": "^2.5|^3",
@@ -8510,11 +8426,11 @@
                 }
             ],
             "description": "Symfony Security Component - Core Library",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/security-core/tree/v6.3.3"
+                "source": "https://github.com/symfony/security-core/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -8526,11 +8442,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-31T07:08:24+00:00"
+            "time": "2023-09-10T17:47:23+00:00"
         },
         {
             "name": "symfony/security-csrf",
             "version": "v6.3.2",
             "source": {
@@ -8598,20 +8514,20 @@
             ],
             "time": "2023-07-05T08:41:27+00:00"
         },
         {
             "name": "symfony/security-http",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/security-http.git",
-                "reference": "0afb37c1120c1c46219bdbd1dd912fb4d48eaf7d"
+                "reference": "47058ea557a4c64ba86e9249651222842bd52e2a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/security-http/zipball/0afb37c1120c1c46219bdbd1dd912fb4d48eaf7d",
-                "reference": "0afb37c1120c1c46219bdbd1dd912fb4d48eaf7d",
+                "url": "https://api.github.com/repos/symfony/security-http/zipball/47058ea557a4c64ba86e9249651222842bd52e2a",
+                "reference": "47058ea557a4c64ba86e9249651222842bd52e2a",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/deprecation-contracts": "^2.5|^3",
@@ -8665,11 +8581,11 @@
                 }
             ],
             "description": "Symfony Security Component - HTTP Integration",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/security-http/tree/v6.3.4"
+                "source": "https://github.com/symfony/security-http/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -8681,24 +8597,24 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-25T19:43:09+00:00"
+            "time": "2023-08-30T06:30:46+00:00"
         },
         {
             "name": "symfony/serializer",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/serializer.git",
-                "reference": "96d28a58d5a128bf77c54534b380eb7c92c8f846"
+                "reference": "855fc058c8bdbb69f53834f2fdb3876c9bc0ab7c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/serializer/zipball/96d28a58d5a128bf77c54534b380eb7c92c8f846",
-                "reference": "96d28a58d5a128bf77c54534b380eb7c92c8f846",
+                "url": "https://api.github.com/repos/symfony/serializer/zipball/855fc058c8bdbb69f53834f2fdb3876c9bc0ab7c",
+                "reference": "855fc058c8bdbb69f53834f2fdb3876c9bc0ab7c",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/deprecation-contracts": "^2.5|^3",
@@ -8759,11 +8675,11 @@
                 }
             ],
             "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/serializer/tree/v6.3.4"
+                "source": "https://github.com/symfony/serializer/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -8775,11 +8691,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-24T14:35:28+00:00"
+            "time": "2023-09-29T16:18:53+00:00"
         },
         {
             "name": "symfony/service-contracts",
             "version": "v3.3.0",
             "source": {
@@ -8923,20 +8839,20 @@
             ],
             "time": "2023-02-16T10:14:28+00:00"
         },
         {
             "name": "symfony/string",
-            "version": "v6.3.2",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/string.git",
-                "reference": "53d1a83225002635bca3482fcbf963001313fb68"
+                "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/string/zipball/53d1a83225002635bca3482fcbf963001313fb68",
-                "reference": "53d1a83225002635bca3482fcbf963001313fb68",
+                "url": "https://api.github.com/repos/symfony/string/zipball/13d76d0fb049051ed12a04bef4f9de8715bea339",
+                "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/polyfill-ctype": "~1.8",
@@ -8989,11 +8905,11 @@
                 "unicode",
                 "utf-8",
                 "utf8"
             ],
             "support": {
-                "source": "https://github.com/symfony/string/tree/v6.3.2"
+                "source": "https://github.com/symfony/string/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -9005,11 +8921,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-05T08:41:27+00:00"
+            "time": "2023-09-18T10:38:32+00:00"
         },
         {
             "name": "symfony/translation",
             "version": "v6.3.3",
             "source": {
@@ -9182,20 +9098,20 @@
             ],
             "time": "2023-05-30T17:17:10+00:00"
         },
         {
             "name": "symfony/twig-bridge",
-            "version": "v6.3.2",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/twig-bridge.git",
-                "reference": "6f8435db76a2d79917489a19a82679276c1b4e32"
+                "reference": "18f2cbe1d46ad43c4d3bd45e5e6279172068e064"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/6f8435db76a2d79917489a19a82679276c1b4e32",
-                "reference": "6f8435db76a2d79917489a19a82679276c1b4e32",
+                "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/18f2cbe1d46ad43c4d3bd45e5e6279172068e064",
+                "reference": "18f2cbe1d46ad43c4d3bd45e5e6279172068e064",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/translation-contracts": "^2.5|^3",
@@ -9270,11 +9186,11 @@
                 }
             ],
             "description": "Provides integration for Twig with various Symfony components",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/twig-bridge/tree/v6.3.2"
+                "source": "https://github.com/symfony/twig-bridge/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -9286,11 +9202,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-20T16:42:33+00:00"
+            "time": "2023-09-12T06:57:20+00:00"
         },
         {
             "name": "symfony/twig-bundle",
             "version": "v6.3.0",
             "source": {
@@ -9375,20 +9291,20 @@
             ],
             "time": "2023-05-06T09:53:41+00:00"
         },
         {
             "name": "symfony/validator",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/validator.git",
-                "reference": "0c8435154920b9bbe93bece675234c244cadf73b"
+                "reference": "48e815ba3b5eb72e632588dbf7ea2dc4e608ee47"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/validator/zipball/0c8435154920b9bbe93bece675234c244cadf73b",
-                "reference": "0c8435154920b9bbe93bece675234c244cadf73b",
+                "url": "https://api.github.com/repos/symfony/validator/zipball/48e815ba3b5eb72e632588dbf7ea2dc4e608ee47",
+                "reference": "48e815ba3b5eb72e632588dbf7ea2dc4e608ee47",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/deprecation-contracts": "^2.5|^3",
@@ -9451,11 +9367,11 @@
                 }
             ],
             "description": "Provides tools to validate values",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/validator/tree/v6.3.4"
+                "source": "https://github.com/symfony/validator/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -9467,24 +9383,24 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-17T15:49:05+00:00"
+            "time": "2023-09-29T07:41:15+00:00"
         },
         {
             "name": "symfony/var-dumper",
-            "version": "v6.3.4",
+            "version": "v6.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-                "reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45"
+                "reference": "3d9999376be5fea8de47752837a3e1d1c5f69ef5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2027be14f8ae8eae999ceadebcda5b4909b81d45",
-                "reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/3d9999376be5fea8de47752837a3e1d1c5f69ef5",
+                "reference": "3d9999376be5fea8de47752837a3e1d1c5f69ef5",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
                 "symfony/deprecation-contracts": "^2.5|^3",
@@ -9535,11 +9451,11 @@
             "keywords": [
                 "debug",
                 "dump"
             ],
             "support": {
-                "source": "https://github.com/symfony/var-dumper/tree/v6.3.4"
+                "source": "https://github.com/symfony/var-dumper/tree/v6.3.5"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -9551,11 +9467,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-24T14:51:05+00:00"
+            "time": "2023-09-12T10:11:35+00:00"
         },
         {
             "name": "symfony/var-exporter",
             "version": "v6.3.4",
             "source": {
@@ -9629,35 +9545,35 @@
             ],
             "time": "2023-08-16T18:14:47+00:00"
         },
         {
             "name": "symfony/webpack-encore-bundle",
-            "version": "v2.0.1",
+            "version": "v2.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/webpack-encore-bundle.git",
-                "reference": "150fe022740fef908f4ca3d5950ce85ab040ec76"
+                "reference": "50d4af5cca35dab66a51dfeeb63616937ba96f4a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/150fe022740fef908f4ca3d5950ce85ab040ec76",
-                "reference": "150fe022740fef908f4ca3d5950ce85ab040ec76",
+                "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/50d4af5cca35dab66a51dfeeb63616937ba96f4a",
+                "reference": "50d4af5cca35dab66a51dfeeb63616937ba96f4a",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1.0",
-                "symfony/asset": "^5.4 || ^6.2",
-                "symfony/config": "^5.4 || ^6.2",
-                "symfony/dependency-injection": "^5.4 || ^6.2",
-                "symfony/http-kernel": "^5.4 || ^6.2",
+                "symfony/asset": "^5.4 || ^6.2 || ^7.0",
+                "symfony/config": "^5.4 || ^6.2 || ^7.0",
+                "symfony/dependency-injection": "^5.4 || ^6.2 || ^7.0",
+                "symfony/http-kernel": "^5.4 || ^6.2 || ^7.0",
                 "symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0"
             },
             "require-dev": {
-                "symfony/framework-bundle": "^5.4 || ^6.2",
-                "symfony/phpunit-bridge": "^5.4 || ^6.2",
-                "symfony/twig-bundle": "^5.4 || ^6.2",
-                "symfony/web-link": "^5.4 || ^6.2"
+                "symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0",
+                "symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0",
+                "symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0",
+                "symfony/web-link": "^5.4 || ^6.2 || ^7.0"
             },
             "type": "symfony-bundle",
             "extra": {
                 "thanks": {
                     "name": "symfony/webpack-encore",
@@ -9680,11 +9596,11 @@
                 }
             ],
             "description": "Integration with your Symfony app & Webpack Encore!",
             "support": {
                 "issues": "https://github.com/symfony/webpack-encore-bundle/issues",
-                "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.0.1"
+                "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.1.0"
             },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "custom"
@@ -9696,11 +9612,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-05-31T14:28:33+00:00"
+            "time": "2023-09-26T14:41:29+00:00"
         },
         {
             "name": "symfony/yaml",
             "version": "v6.3.3",
             "source": {
@@ -10389,20 +10305,20 @@
             },
             "time": "2022-01-30T20:08:53+00:00"
         },
         {
             "name": "zircote/swagger-php",
-            "version": "4.7.14",
+            "version": "4.7.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/zircote/swagger-php.git",
-                "reference": "e53c0c7a6e250c435cc13ab50792247a8eb07da3"
+                "reference": "df8de8e484003f68cd2fa68db1e6cfb47a3a92cb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/zircote/swagger-php/zipball/e53c0c7a6e250c435cc13ab50792247a8eb07da3",
-                "reference": "e53c0c7a6e250c435cc13ab50792247a8eb07da3",
+                "url": "https://api.github.com/repos/zircote/swagger-php/zipball/df8de8e484003f68cd2fa68db1e6cfb47a3a92cb",
+                "reference": "df8de8e484003f68cd2fa68db1e6cfb47a3a92cb",
                 "shasum": ""
             },
             "require": {
                 "doctrine/annotations": "^1.7 || ^2.0",
                 "ext-json": "*",
@@ -10461,28 +10377,28 @@
                 "rest",
                 "service discovery"
             ],
             "support": {
                 "issues": "https://github.com/zircote/swagger-php/issues",
-                "source": "https://github.com/zircote/swagger-php/tree/4.7.14"
+                "source": "https://github.com/zircote/swagger-php/tree/4.7.15"
             },
-            "time": "2023-09-12T00:18:13+00:00"
+            "time": "2023-10-12T20:26:34+00:00"
         }
     ],
     "packages-dev": [
         {
             "name": "composer/pcre",
-            "version": "3.1.0",
+            "version": "3.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/pcre.git",
-                "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2"
+                "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2",
-                "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2",
+                "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9",
+                "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.4 || ^8.0"
             },
@@ -10520,11 +10436,11 @@
                 "regex",
                 "regular expression"
             ],
             "support": {
                 "issues": "https://github.com/composer/pcre/issues",
-                "source": "https://github.com/composer/pcre/tree/3.1.0"
+                "source": "https://github.com/composer/pcre/tree/3.1.1"
             },
             "funding": [
                 {
                     "url": "https://packagist.com",
                     "type": "custom"
@@ -10536,11 +10452,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/composer/composer",
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-17T09:50:14+00:00"
+            "time": "2023-10-11T07:11:09+00:00"
         },
         {
             "name": "composer/xdebug-handler",
             "version": "3.0.3",
             "source": {
@@ -10906,20 +10822,20 @@
             },
             "time": "2023-06-12T08:44:38+00:00"
         },
         {
             "name": "friendsofphp/php-cs-fixer",
-            "version": "v3.27.0",
+            "version": "v3.35.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
-                "reference": "e73ccaae1208f017bb7860986eebb3da48bd25d6"
+                "reference": "ec1ccc264994b6764882669973ca435cf05bab08"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/e73ccaae1208f017bb7860986eebb3da48bd25d6",
-                "reference": "e73ccaae1208f017bb7860986eebb3da48bd25d6",
+                "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/ec1ccc264994b6764882669973ca435cf05bab08",
+                "reference": "ec1ccc264994b6764882669973ca435cf05bab08",
                 "shasum": ""
             },
             "require": {
                 "composer/semver": "^3.3",
                 "composer/xdebug-handler": "^3.0.3",
@@ -10948,12 +10864,10 @@
                 "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2",
                 "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1",
                 "phpspec/prophecy": "^1.16",
                 "phpspec/prophecy-phpunit": "^2.0",
                 "phpunit/phpunit": "^9.5",
-                "phpunitgoodpractices/polyfill": "^1.6",
-                "phpunitgoodpractices/traits": "^1.9.2",
                 "symfony/phpunit-bridge": "^6.2.3",
                 "symfony/yaml": "^5.4 || ^6.0"
             },
             "suggest": {
                 "ext-dom": "For handling output formats in XML",
@@ -10989,19 +10903,19 @@
                 "standards",
                 "static analysis"
             ],
             "support": {
                 "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
-                "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.27.0"
+                "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.35.1"
             },
             "funding": [
                 {
                     "url": "https://github.com/keradus",
                     "type": "github"
                 }
             ],
-            "time": "2023-09-17T14:37:54+00:00"
+            "time": "2023-10-12T13:47:26+00:00"
         },
         {
             "name": "masterminds/html5",
             "version": "2.8.1",
             "source": {
@@ -11235,20 +11149,20 @@
             },
             "time": "2022-02-21T01:04:05+00:00"
         },
         {
             "name": "phpstan/phpstan",
-            "version": "1.10.34",
+            "version": "1.10.39",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901"
+                "reference": "d9dedb0413f678b4d03cbc2279a48f91592c97c4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7f806b6f1403e6914c778140e2ba07c293cb4901",
-                "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d9dedb0413f678b4d03cbc2279a48f91592c97c4",
+                "reference": "d9dedb0413f678b4d03cbc2279a48f91592c97c4",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.2|^8.0"
             },
@@ -11293,11 +11207,11 @@
                 {
                     "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-09-13T09:49:47+00:00"
+            "time": "2023-10-17T15:46:26+00:00"
         },
         {
             "name": "phpstan/phpstan-doctrine",
             "version": "1.3.43",
             "source": {
@@ -11367,20 +11281,20 @@
             },
             "time": "2023-09-01T15:01:13+00:00"
         },
         {
             "name": "phpstan/phpstan-phpunit",
-            "version": "1.3.14",
+            "version": "1.3.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan-phpunit.git",
-                "reference": "614acc10c522e319639bf38b0698a4a566665f04"
+                "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/614acc10c522e319639bf38b0698a4a566665f04",
-                "reference": "614acc10c522e319639bf38b0698a4a566665f04",
+                "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/70ecacc64fe8090d8d2a33db5a51fe8e88acd93a",
+                "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.2 || ^8.0",
                 "phpstan/phpstan": "^1.10"
@@ -11413,13 +11327,13 @@
                 "MIT"
             ],
             "description": "PHPUnit extensions and rules for PHPStan",
             "support": {
                 "issues": "https://github.com/phpstan/phpstan-phpunit/issues",
-                "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.14"
+                "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.15"
             },
-            "time": "2023-08-25T09:46:39+00:00"
+            "time": "2023-10-09T18:58:39+00:00"
         },
         {
             "name": "phpstan/phpstan-strict-rules",
             "version": "1.5.1",
             "source": {
@@ -11468,46 +11382,47 @@
             },
             "time": "2023-03-29T14:47:40+00:00"
         },
         {
             "name": "phpstan/phpstan-symfony",
-            "version": "1.3.2",
+            "version": "1.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan-symfony.git",
-                "reference": "7332b90dfc291ac5b4b83fbca2081936faa1e3f9"
+                "reference": "383855999db6a7d65d0bf580ce2762e17188c2a5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/7332b90dfc291ac5b4b83fbca2081936faa1e3f9",
-                "reference": "7332b90dfc291ac5b4b83fbca2081936faa1e3f9",
+                "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/383855999db6a7d65d0bf580ce2762e17188c2a5",
+                "reference": "383855999db6a7d65d0bf580ce2762e17188c2a5",
                 "shasum": ""
             },
             "require": {
                 "ext-simplexml": "*",
                 "php": "^7.2 || ^8.0",
-                "phpstan/phpstan": "^1.9.18"
+                "phpstan/phpstan": "^1.10.36"
             },
             "conflict": {
                 "symfony/framework-bundle": "<3.0"
             },
             "require-dev": {
                 "nikic/php-parser": "^4.13.0",
                 "php-parallel-lint/php-parallel-lint": "^1.2",
-                "phpstan/phpstan-phpunit": "^1.0",
-                "phpstan/phpstan-strict-rules": "^1.0",
+                "phpstan/phpstan-phpunit": "^1.3.11",
+                "phpstan/phpstan-strict-rules": "^1.5.1",
                 "phpunit/phpunit": "^8.5.29 || ^9.5",
                 "psr/container": "1.0 || 1.1.1",
                 "symfony/config": "^5.4 || ^6.1",
                 "symfony/console": "^5.4 || ^6.1",
                 "symfony/dependency-injection": "^5.4 || ^6.1",
                 "symfony/form": "^5.4 || ^6.1",
                 "symfony/framework-bundle": "^5.4 || ^6.1",
                 "symfony/http-foundation": "^5.4 || ^6.1",
                 "symfony/messenger": "^5.4",
                 "symfony/polyfill-php80": "^1.24",
-                "symfony/serializer": "^5.4"
+                "symfony/serializer": "^5.4",
+                "symfony/service-contracts": "^2.2.0"
             },
             "type": "phpstan-extension",
             "extra": {
                 "phpstan": {
                     "includes": [
@@ -11533,26 +11448,26 @@
                 }
             ],
             "description": "Symfony Framework extensions and rules for PHPStan",
             "support": {
                 "issues": "https://github.com/phpstan/phpstan-symfony/issues",
-                "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.2"
+                "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.4"
             },
-            "time": "2023-05-16T12:46:15+00:00"
+            "time": "2023-09-29T14:10:11+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "9.2.28",
+            "version": "9.2.29",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "7134a5ccaaf0f1c92a4f5501a6c9f98ac4dcc0ef"
+                "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7134a5ccaaf0f1c92a4f5501a6c9f98ac4dcc0ef",
-                "reference": "7134a5ccaaf0f1c92a4f5501a6c9f98ac4dcc0ef",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76",
+                "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-libxml": "*",
@@ -11605,19 +11520,19 @@
                 "xunit"
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
                 "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.28"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29"
             },
             "funding": [
                 {
                     "url": "https://github.com/sebastianbergmann",
                     "type": "github"
                 }
             ],
-            "time": "2023-09-12T14:36:20+00:00"
+            "time": "2023-09-19T04:57:46+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
             "version": "3.0.6",
             "source": {
diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml
index 468d9874..78baf712 100644
--- a/config/packages/doctrine.yaml
+++ b/config/packages/doctrine.yaml
@@ -16,18 +16,20 @@ doctrine:
                 driver: 'pdo_mysql'
                 charset: utf8mb4
                 default_table_options:
                     charset: utf8mb4
                     collate: utf8mb4_unicode_ci
+                schema_manager_factory: doctrine.dbal.default_schema_manager_factory
 
         types:
             datetime: App\Doctrine\UTCDateTimeType
     orm:
         auto_generate_proxy_classes: '%kernel.debug%'
         default_entity_manager: default
         entity_managers:
             default:
+                report_fields_where_declared: true
                 connection: default
                 naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                 auto_mapping: true
                 mappings:
                     App:
diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml
index f4ab8981..30a2df2e 100644
--- a/config/packages/framework.yaml
+++ b/config/packages/framework.yaml
@@ -15,12 +15,10 @@ framework:
         cookie_secure: auto
         cookie_samesite: lax
         cookie_httponly: true
         storage_factory_id: session.storage.factory.native
 
-    #esi: ~
-    #fragments: ~
     php_errors:
         log: true
 
     mailer:
         dsn: '%env(MAILER_URL)%'
diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml
index cda11570..b50736cc 100644
--- a/config/packages/kimai.yaml
+++ b/config/packages/kimai.yaml
@@ -149,15 +149,17 @@ kimai:
 
 
 # --------------------------------------------------------------------------------
 # INVOICES
 # --------------------------------------------------------------------------------
-#    invoice:
-#        # all files in these directories will be used as invoice documents (if supported by a renderer)
-#        documents:
-#            - 'var/invoices/'
-#            - 'templates/invoice/renderer/'
+#   invoice:
+#       # all files in these directories will be used as invoice documents (if supported by a renderer)
+#       documents:
+#           - 'var/invoices/'
+#           - 'templates/invoice/renderer/'
+#       # allow to upload twig templates as invoice documents
+#       upload_twig: true
 # --------------------------------------------------------------------------------
 
 
 when@test:
     kimai:
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index c7b5e49c..3237af0a 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -63,10 +63,16 @@ security:
 
             login_throttling:
                 max_attempts: 5
                 interval: '5 minutes'
 
+            login_link:
+                check_route: link_login_check
+                signature_properties: ['id']
+                lifetime: 300
+                max_uses: 1
+
     access_decision_manager:
         strategy: unanimous
         allow_if_all_abstain: false
 
     role_hierarchy:
diff --git a/config/routes.yaml b/config/routes.yaml
index 27597722..af7c31f3 100644
--- a/config/routes.yaml
+++ b/config/routes.yaml
@@ -1,8 +1,8 @@
 controllers:
     resource: ../src/Controller/
-    type: annotation
+    type: attribute
     prefix: /{_locale}
     requirements:
         _locale: '%app_locales%'
     defaults:
         _locale: '%locale%'
@@ -17,30 +17,30 @@ api.swagger:
     methods: GET
     defaults: { _controller: nelmio_api_doc.controller.swagger }
 
 api:
     resource: ../src/API/
-    type: annotation
+    type: attribute
     prefix: /api
 
 auth:
     resource: ../src/Controller/Auth/
-    type: annotation
+    type: attribute
     prefix: /auth
 
 security:
     resource: ../src/Controller/Security/
-    type: annotation
+    type: attribute
     prefix: /{_locale}
     requirements:
         _locale: '%app_locales%'
     defaults:
         _locale: '%locale%'
 
 kernel:
     resource: ../src/Kernel.php
-    type: annotation
+    type: attribute
 
 home:
     path: /
     defaults:
         _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
diff --git a/config/services.yaml b/config/services.yaml
index 3d027024..6637ab24 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -89,20 +89,14 @@ services:
 
     # updates timesheet records and apply configured rate & rounding rules
     App\Doctrine\TimesheetSubscriber:
         class: App\Doctrine\TimesheetSubscriber
         arguments: [!tagged timesheet.calculator]
-        tags:
-            - { name: doctrine.event_subscriber, priority: 50 }
 
     # updates timestampable columns (higher priority, so the TimesheetSubscriber will be executed later)
-    Gedmo\Timestampable\TimestampableListener:
-        class: Gedmo\Timestampable\TimestampableListener
-        tags:
-            - { name: doctrine.event_subscriber, priority: 60 }
-        calls:
-            - [ setAnnotationReader, [ "@annotation_reader" ] ]
+    App\Doctrine\ModifiedSubscriber:
+        class: App\Doctrine\ModifiedSubscriber
 
     # ================================================================================
     # TIMESHEET RECORD CALCULATOR
     # ================================================================================
 
diff --git a/phpstan.neon b/phpstan.neon
index 6f9b535c..c51d0183 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -926,11 +926,11 @@ parameters:
             count: 3
             path: src/Controller/BookmarkController.php
 
         -
             message: "#^Cannot access offset 'user' on mixed\\.$#"
-            count: 2
+            count: 1
             path: src/Controller/CalendarController.php
 
         -
             message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
             count: 1
@@ -2576,16 +2576,16 @@ parameters:
             count: 1
             path: src/Form/MultiUpdate/MultiUpdateTable.php
 
         -
             message: "#^Cannot access offset 'customer' on mixed\\.$#"
-            count: 3
+            count: 1
             path: src/Form/MultiUpdate/TimesheetMultiUpdate.php
 
         -
             message: "#^Cannot access offset 'project' on mixed\\.$#"
-            count: 6
+            count: 2
             path: src/Form/MultiUpdate/TimesheetMultiUpdate.php
 
         -
             message: "#^Cannot call method getLabel\\(\\) on App\\\\Entity\\\\MetaTableTypeInterface\\|null\\.$#"
             count: 1
@@ -3601,16 +3601,16 @@ parameters:
             count: 1
             path: src/Form/Type/QuickEntryTimesheetType.php
 
         -
             message: "#^Cannot access offset 'activity' on mixed\\.$#"
-            count: 3
+            count: 1
             path: src/Form/Type/QuickEntryWeekType.php
 
         -
             message: "#^Cannot access offset 'project' on mixed\\.$#"
-            count: 3
+            count: 1
             path: src/Form/Type/QuickEntryWeekType.php
 
         -
             message: "#^Cannot clone mixed\\.$#"
             count: 1
@@ -4291,11 +4291,11 @@ parameters:
             count: 8
             path: src/Project/ProjectStatisticService.php
 
         -
             message: "#^Cannot access offset 'lastRecord' on mixed\\.$#"
-            count: 2
+            count: 1
             path: src/Project/ProjectStatisticService.php
 
         -
             message: "#^Cannot access offset 'rate' on mixed\\.$#"
             count: 6
@@ -4436,11 +4436,11 @@ parameters:
             count: 1
             path: src/Reporting/ProjectDateRange/ProjectDateRangeQuery.php
 
         -
             message: "#^Cannot access offset 'project' on mixed\\.$#"
-            count: 3
+            count: 1
             path: src/Reporting/ProjectDetails/ProjectDetailsForm.php
 
         -
             message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Activity\\|null\\.$#"
             count: 1
@@ -5714,15 +5714,10 @@ parameters:
         -
             message: "#^Method App\\\\Utils\\\\MenuItemModel\\:\\:__construct\\(\\) has parameter \\$routeArgs with no value type specified in iterable type array\\.$#"
             count: 1
             path: src/Utils/MenuItemModel.php
 
-        -
-            message: "#^Method App\\\\Utils\\\\MenuItemModel\\:\\:getRouteArgs\\(\\) return type has no value type specified in iterable type array\\.$#"
-            count: 1
-            path: src/Utils/MenuItemModel.php
-
         -
             message: "#^Method App\\\\Utils\\\\MenuItemModel\\:\\:setChildRoutes\\(\\) has parameter \\$routes with no value type specified in iterable type array\\.$#"
             count: 1
             path: src/Utils/MenuItemModel.php
 
diff --git a/src/API/ActionsController.php b/src/API/ActionsController.php
index e7f6a4dc..e561d0e5 100644
--- a/src/API/ActionsController.php
+++ b/src/API/ActionsController.php
@@ -19,14 +19,14 @@ use FOS\RestBundle\Controller\Annotations as Rest;
 use FOS\RestBundle\View\View;
 use FOS\RestBundle\View\ViewHandlerInterface;
 use Nelmio\ApiDocBundle\Annotation\Model;
 use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
 use OpenApi\Attributes as OA;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
 #[Route(path: '/actions')]
 #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
 #[OA\Tag(name: 'Actions')]
diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php
index f3555339..fe72917e 100644
--- a/src/API/ActivityController.php
+++ b/src/API/ActivityController.php
@@ -24,12 +24,12 @@ use FOS\RestBundle\Controller\Annotations as Rest;
 use FOS\RestBundle\Request\ParamFetcherInterface;
 use FOS\RestBundle\View\View;
 use FOS\RestBundle\View\ViewHandlerInterface;
 use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
 use OpenApi\Attributes as OA;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Bridge\Doctrine\Attribute\MapEntity;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 
diff --git a/src/API/BaseApiController.php b/src/API/BaseApiController.php
index 6cdfdee7..490904cc 100644
--- a/src/API/BaseApiController.php
+++ b/src/API/BaseApiController.php
@@ -8,16 +8,16 @@
  */
 
 namespace App\API;
 
 use App\Entity\User;
+use App\Repository\Query\BaseQuery;
 use App\Timesheet\DateTimeFactory;
 use App\Utils\Pagination;
 use FOS\RestBundle\View\View;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
-use Symfony\Component\Form\Extension\Core\Type\FormType;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\Form\FormTypeInterface;
 
 /**
  * @method null|User getUser()
@@ -28,17 +28,17 @@ abstract class BaseApiController extends AbstractController
     public const DATE_FORMAT = DateTimeType::HTML5_FORMAT;
     public const DATE_FORMAT_PHP = 'Y-m-d\TH:i:s';
 
     /**
      * @template TFormType of FormTypeInterface<TData>
-     * @template TData of mixed
+     * @template TData of BaseQuery
      * @param class-string<TFormType> $type
-     * @param TData|null $data
+     * @param TData $data
      * @param array<mixed> $options
-     * @return FormInterface<TData|null>
+     * @return FormInterface<BaseQuery>
      */
-    protected function createSearchForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
+    protected function createSearchForm(string $type, BaseQuery $data, array $options = []): FormInterface
     {
         return $this->container
             ->get('form.factory')
             ->createNamed('', $type, $data, array_merge(['method' => 'GET'], $options));
     }
diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php
index 24d96428..55cab78d 100644
--- a/src/API/CustomerController.php
+++ b/src/API/CustomerController.php
@@ -24,12 +24,12 @@ use FOS\RestBundle\Controller\Annotations as Rest;
 use FOS\RestBundle\Request\ParamFetcherInterface;
 use FOS\RestBundle\View\View;
 use FOS\RestBundle\View\ViewHandlerInterface;
 use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
 use OpenApi\Attributes as OA;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Bridge\Doctrine\Attribute\MapEntity;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 
diff --git a/src/API/ProjectController.php b/src/API/ProjectController.php
index 3e0ae95b..89e636cf 100644
--- a/src/API/ProjectController.php
+++ b/src/API/ProjectController.php
@@ -25,12 +25,12 @@ use FOS\RestBundle\Controller\Annotations as Rest;
 use FOS\RestBundle\Request\ParamFetcherInterface;
 use FOS\RestBundle\View\View;
 use FOS\RestBundle\View\ViewHandlerInterface;
 use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
 use OpenApi\Attributes as OA;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Bridge\Doctrine\Attribute\MapEntity;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 use Symfony\Component\Validator\Constraints;
diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php
index aa81ab1c..8e2fa9e5 100644
--- a/src/API/TimesheetController.php
+++ b/src/API/TimesheetController.php
@@ -31,18 +31,18 @@ use FOS\RestBundle\Controller\Annotations as Rest;
 use FOS\RestBundle\Request\ParamFetcherInterface;
 use FOS\RestBundle\View\View;
 use FOS\RestBundle\View\ViewHandlerInterface;
 use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
 use OpenApi\Attributes as OA;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\ExpressionLanguage\Expression;
 use Symfony\Component\Form\FormError;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 use Symfony\Component\Validator\Constraints;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 #[Route(path: '/timesheets')]
 #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
 #[OA\Tag(name: 'Timesheet')]
 final class TimesheetController extends BaseApiController
@@ -91,11 +91,11 @@ final class TimesheetController extends BaseApiController
     #[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5)')]
     #[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5)')]
     #[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')]
     #[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
     #[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
-    #[Rest\QueryParam(name: 'full', strict: true, nullable: true, description: 'Allows to fetch fully serialized objects including subresources. Allowed values: true (default: false)')]
+    #[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
     #[Rest\QueryParam(name: 'term', description: 'Free search term')]
     #[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5). Available since Kimai 1.10 and works only for records that were created/updated since then.')]
     public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
     {
         $query = new TimesheetQuery(false);
@@ -255,11 +255,12 @@ final class TimesheetController extends BaseApiController
         $results = (array) $data->getCurrentPageResults();
 
         $view = new View($results, 200);
         $this->addPagination($view, $data);
 
-        if (null !== $paramFetcher->get('full')) {
+        $full = $paramFetcher->get('full');
+        if ($full === '1' || $full === 'true') {
             $view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
         } else {
             $view->getContext()->setGroups(self::GROUPS_COLLECTION);
         }
 
diff --git a/src/API/UserController.php b/src/API/UserController.php
index 99246e40..a02fcf5e 100644
--- a/src/API/UserController.php
+++ b/src/API/UserController.php
@@ -21,11 +21,11 @@ use FOS\RestBundle\Controller\Annotations as Rest;
 use FOS\RestBundle\Request\ParamFetcherInterface;
 use FOS\RestBundle\View\View;
 use FOS\RestBundle\View\ViewHandlerInterface;
 use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
 use OpenApi\Attributes as OA;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
 use Symfony\Component\Routing\Annotation\Route;
@@ -37,10 +37,11 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
 final class UserController extends BaseApiController
 {
     public const GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
     public const GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
     public const GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
+    public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'User', 'User_Entity'];
 
     public function __construct(
         private ViewHandlerInterface $viewHandler,
         private UserRepository $repository,
         private UserPasswordHasherInterface $passwordHasher,
@@ -58,10 +59,11 @@ final class UserController extends BaseApiController
     #[ApiSecurity(name: 'apiToken')]
     #[Rest\QueryParam(name: 'visible', requirements: '1|2|3', default: 1, strict: true, nullable: true, description: 'Visibility status to filter users: 1=visible, 2=hidden, 3=all')]
     #[Rest\QueryParam(name: 'orderBy', requirements: 'id|username|alias|email', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, username, alias, email (default: username)')]
     #[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
     #[Rest\QueryParam(name: 'term', description: 'Free search term')]
+    #[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
     public function cgetAction(ParamFetcherInterface $paramFetcher): Response
     {
         $query = new UserQuery();
         $query->setCurrentUser($this->getUser());
 
@@ -86,11 +88,17 @@ final class UserController extends BaseApiController
         }
 
         $query->setIsApiCall(true);
         $data = $this->repository->getUsersForQuery($query);
         $view = new View($data, 200);
-        $view->getContext()->setGroups(self::GROUPS_COLLECTION);
+
+        $full = $paramFetcher->get('full');
+        if ($full === '1' || $full === 'true') {
+            $view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
+        } else {
+            $view->getContext()->setGroups(self::GROUPS_COLLECTION);
+        }
 
         return $this->viewHandler->handle($view);
     }
 
     /**
diff --git a/src/Activity/ActivityService.php b/src/Activity/ActivityService.php
index dbb2e9d1..c82c41d1 100644
--- a/src/Activity/ActivityService.php
+++ b/src/Activity/ActivityService.php
@@ -18,11 +18,11 @@ use App\Event\ActivityMetaDefinitionEvent;
 use App\Event\ActivityUpdatePostEvent;
 use App\Event\ActivityUpdatePreEvent;
 use App\Repository\ActivityRepository;
 use App\Validator\ValidationFailedException;
 use InvalidArgumentException;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Validator\Validator\ValidatorInterface;
 
 /**
  * @final
  */
diff --git a/src/Calendar/CalendarService.php b/src/Calendar/CalendarService.php
index b0a5cede..9ffe6037 100644
--- a/src/Calendar/CalendarService.php
+++ b/src/Calendar/CalendarService.php
@@ -16,11 +16,11 @@ use App\Event\CalendarDragAndDropSourceEvent;
 use App\Event\CalendarGoogleSourceEvent;
 use App\Event\CalendarSourceEvent;
 use App\Event\RecentActivityEvent;
 use App\Repository\TimesheetRepository;
 use App\Utils\Color;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 final class CalendarService
 {
     public function __construct(private SystemConfiguration $configuration, private TimesheetRepository $repository, private EventDispatcherInterface $dispatcher)
     {
diff --git a/src/Command/DemoteUserCommand.php b/src/Command/DemoteUserCommand.php
index eb2c218b..99717e6b 100644
--- a/src/Command/DemoteUserCommand.php
+++ b/src/Command/DemoteUserCommand.php
@@ -31,13 +31,10 @@ final class DemoteUserCommand extends AbstractRoleCommand
                       <info>php %command.full_name% --super susan_super</info>
                     EOT
             );
     }
 
-    /**
-     * {@inheritdoc}
-     */
     protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
     {
         $username = $user->getUserIdentifier();
         if ($super) {
             if ($user->isSuperAdmin()) {
diff --git a/src/Command/InvoiceCreateCommand.php b/src/Command/InvoiceCreateCommand.php
index 0066b5dc..a88bbe16 100644
--- a/src/Command/InvoiceCreateCommand.php
+++ b/src/Command/InvoiceCreateCommand.php
@@ -20,21 +20,21 @@ use App\Repository\ProjectRepository;
 use App\Repository\Query\InvoiceQuery;
 use App\Repository\Query\TimesheetQuery;
 use App\Repository\UserRepository;
 use App\Timesheet\DateTimeFactory;
 use App\Utils\SearchTerm;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Console\Attribute\AsCommand;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Helper\Table;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
 use Symfony\Component\Filesystem\Filesystem;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Symfony\Component\HttpFoundation\Response;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 #[AsCommand(name: 'kimai:invoice:create')]
 final class InvoiceCreateCommand extends Command
 {
     private ?string $previewDirectory = null;
diff --git a/src/Command/UserLoginLinkCommand.php b/src/Command/UserLoginLinkCommand.php
new file mode 100644
index 00000000..3da06baa
--- /dev/null
+++ b/src/Command/UserLoginLinkCommand.php
@@ -0,0 +1,89 @@
+<?php
+
+/*
+ * This file is part of the Kimai time-tracking app.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Command;
+
+use App\Repository\UserRepository;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
+
+#[AsCommand(name: 'kimai:user:login-link', description: 'Create a URL that can be used to login as that user', hidden: true)]
+/**
+ * @CloudRequired
+ */
+final class UserLoginLinkCommand extends Command
+{
+    public function __construct(
+        private LoginLinkHandlerInterface $loginLink,
+        private UserRepository $userRepository,
+        private RequestStack $requestStack
+    )
+    {
+        parent::__construct();
+        $this->addArgument('email', InputArgument::REQUIRED, 'The email of the user');
+        $this->addOption('password-reset', null, InputOption::VALUE_NONE, 'Whether the user needs to reset the password afterwards');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $io = new SymfonyStyle($input, $output);
+
+        $email = $input->getArgument('email');
+        if ($email === null || $email === '') {
+            $io->error('Need email to create login URL');
+
+            return Command::FAILURE;
+        }
+
+        $user = $this->userRepository->findOneBy(['email' => $email]);
+
+        if ($user === null) {
+            $io->error('Need username to create login URL');
+
+            return Command::FAILURE;
+        }
+
+        if (!$user->isEnabled()) {
+            $io->error('User is not enabled');
+
+            return Command::FAILURE;
+        }
+
+        if (!$user->isInternalUser()) {
+            $io->error('User does not use internal login');
+
+            return Command::FAILURE;
+        }
+
+        $request = new Request();
+        $request->setLocale($user->getLocale());
+        $this->requestStack->push($request);
+
+        $loginLinkDetails = $this->loginLink->createLoginLink($user, $request);
+        $loginLink = $loginLinkDetails->getUrl();
+
+        if ($input->getOption('password-reset') === true) {
+            $user->setPasswordRequestedAt(new \DateTime());
+            $user->setRequiresPasswordReset(true);
+            $this->userRepository->saveUser($user);
+        }
+
+        $output->writeln($loginLink);
+
+        return Command::SUCCESS;
+    }
+}
diff --git a/src/Constants.php b/src/Constants.php
index 9b6a49bb..2cfbf1b9 100644
--- a/src/Constants.php
+++ b/src/Constants.php
@@ -15,25 +15,25 @@ namespace App;
 class Constants
 {
     /**
      * The current release version
      */
-    public const VERSION = '2.0.35';
+    public const VERSION = '2.1.0';
     /**
      * The current release: major * 10000 + minor * 100 + patch
      */
-    public const VERSION_ID = 20035;
+    public const VERSION_ID = 20100;
     /**
      * The software name
      */
     public const SOFTWARE = 'Kimai';
     /**
      * Used in multiple views
      */
     public const GITHUB = 'https://github.com/kimai/kimai/';
     /**
-     * The Github repository name
+     * The GitHub repository name
      */
     public const GITHUB_REPO = 'kimai/kimai';
     /**
      * Homepage, used in multiple views
      */
diff --git a/src/Controller/AbstractController.php b/src/Controller/AbstractController.php
index abdcb774..8f6c0b44 100644
--- a/src/Controller/AbstractController.php
+++ b/src/Controller/AbstractController.php
@@ -15,11 +15,10 @@ use App\Repository\BookmarkRepository;
 use App\Repository\Query\BaseQuery;
 use App\Timesheet\DateTimeFactory;
 use App\Validator\ValidationFailedException;
 use Psr\Log\LoggerInterface;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
-use Symfony\Component\Form\Extension\Core\Type\FormType;
 use Symfony\Component\Form\FormError;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\Form\FormTypeInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -74,17 +73,17 @@ abstract class AbstractController extends BaseAbstractController implements Serv
         return $this->container->get('form.factory')->createNamed('', $type, $data, array_merge(['method' => 'GET'], $options)); // @phpstan-ignore-line
     }
 
     /**
      * @template TFormType of FormTypeInterface<TData>
-     * @template TData of mixed
+     * @template TData of array|object
      * @param class-string<TFormType> $type
-     * @param TData|null $data
+     * @param TData $data
      * @param array<mixed> $options
-     * @return FormInterface<TData|null>
+     * @return FormInterface<TData>
      */
-    protected function createFormWithName(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
+    protected function createFormWithName(string $name, string $type, mixed $data, array $options = []): FormInterface
     {
         return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
     }
 
     /**
diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php
index b406d73f..ccf0fdca 100644
--- a/src/Controller/ActivityController.php
+++ b/src/Controller/ActivityController.php
@@ -33,11 +33,11 @@ use App\Repository\ActivityRepository;
 use App\Repository\Query\ActivityQuery;
 use App\Repository\TeamRepository;
 use App\Utils\DataTable;
 use App\Utils\PageSetup;
 use Exception;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\ExpressionLanguage\Expression;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
diff --git a/src/Controller/ContractController.php b/src/Controller/ContractController.php
index 1e5ceb99..fb44f1af 100644
--- a/src/Controller/ContractController.php
+++ b/src/Controller/ContractController.php
@@ -14,14 +14,14 @@ use App\Event\WorkContractDetailControllerEvent;
 use App\Form\ContractByUserForm;
 use App\Reporting\YearByUser\YearByUser;
 use App\Utils\PageSetup;
 use App\WorkingTime\Model\BoxConfiguration;
 use App\WorkingTime\WorkingTimeService;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 /**
  * Users can control their working time statistics
  */
 final class ContractController extends AbstractController
diff --git a/src/Controller/CustomerController.php b/src/Controller/CustomerController.php
index c130442d..41f03040 100644
--- a/src/Controller/CustomerController.php
+++ b/src/Controller/CustomerController.php
@@ -34,11 +34,11 @@ use App\Repository\ProjectRepository;
 use App\Repository\Query\CustomerQuery;
 use App\Repository\Query\ProjectQuery;
 use App\Repository\TeamRepository;
 use App\Utils\DataTable;
 use App\Utils\PageSetup;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\ExpressionLanguage\Expression;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php
index d58ced45..1b459f1b 100644
--- a/src/Controller/DashboardController.php
+++ b/src/Controller/DashboardController.php
@@ -14,11 +14,11 @@ use App\Entity\User;
 use App\Event\DashboardEvent;
 use App\Repository\BookmarkRepository;
 use App\Utils\PageSetup;
 use App\Widget\WidgetInterface;
 use App\Widget\WidgetService;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
diff --git a/src/Controller/HomepageController.php b/src/Controller/HomepageController.php
index a45e664a..55120fca 100644
--- a/src/Controller/HomepageController.php
+++ b/src/Controller/HomepageController.php
@@ -11,11 +11,11 @@ namespace App\Controller;
 
 use App\Configuration\LocaleService;
 use App\Entity\User;
 use App\Event\ConfigureMainMenuEvent;
 use App\Repository\UserRepository;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 
diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php
index 9c85bce5..7ea0910b 100644
--- a/src/Controller/InvoiceController.php
+++ b/src/Controller/InvoiceController.php
@@ -36,21 +36,21 @@ use App\Repository\Query\BaseQuery;
 use App\Repository\Query\InvoiceArchiveQuery;
 use App\Repository\Query\InvoiceQuery;
 use App\Utils\DataTable;
 use App\Utils\PageSetup;
 use Exception;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\ExpressionLanguage\Expression;
 use Symfony\Component\Form\Extension\Core\Type\FormType;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Csrf\CsrfToken;
 use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 use Twig\Environment;
 
 /**
  * Controller used to create invoices and manage invoice templates.
  */
@@ -466,40 +466,10 @@ final class InvoiceController extends AbstractController
         }
 
         throw $this->createNotFoundException('Unknown document: ' . $document);
     }
 
-    #[Route(path: '/document_reload/{document}', name: 'admin_invoice_document_reload', methods: ['GET', 'POST'])]
-    #[IsGranted('upload_invoice_template')]
-    public function reloadDocument(string $document, Environment $twig): Response
-    {
-        $event = new InvoiceDocumentsEvent($this->service->getDocuments(true));
-        $this->dispatcher->dispatch($event);
-
-        $reloaded = false;
-
-        foreach ($event->getInvoiceDocuments() as $doc) {
-            if ($document === $doc->getId() && $doc->isTwig()) {
-                $reloaded = true;
-                try {
-                    $twig->enableAutoReload();
-                    $twig->load('@invoice/' . basename($doc->getFilename()));
-                    $twig->disableAutoReload();
-                    $this->flashSuccess('Reloaded template');
-                } catch (Exception $ex) {
-                    $this->flashException($ex, 'Failed to reload template: ' . $ex->getMessage());
-                }
-            }
-        }
-
-        if (!$reloaded) {
-            throw $this->createNotFoundException('Unknown document: ' . $document);
-        }
-
-        return $this->redirectToRoute('admin_invoice_document_upload');
-    }
-
     #[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
     #[IsGranted('upload_invoice_template')]
     public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration): Response
     {
         $dir = $documentRepository->getUploadDirectory();
diff --git a/src/Controller/PermissionController.php b/src/Controller/PermissionController.php
index aa8c7a80..e7566710 100644
--- a/src/Controller/PermissionController.php
+++ b/src/Controller/PermissionController.php
@@ -20,11 +20,11 @@ use App\Repository\RoleRepository;
 use App\Repository\UserRepository;
 use App\Security\RolePermissionManager;
 use App\Security\RoleService;
 use App\User\PermissionService;
 use App\Utils\PageSetup;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php
index b288f3d1..7509e69d 100644
--- a/src/Controller/ProfileController.php
+++ b/src/Controller/ProfileController.php
@@ -30,12 +30,12 @@ use Doctrine\Common\Collections\ArrayCollection;
 use Endroid\QrCode\Builder\Builder;
 use Endroid\QrCode\Encoding\Encoding;
 use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
 use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
 use Endroid\QrCode\Writer\PngWriter;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\ExpressionLanguage\Expression;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php
index 390f5736..a8546679 100644
--- a/src/Controller/ProjectController.php
+++ b/src/Controller/ProjectController.php
@@ -38,11 +38,11 @@ use App\Repository\Query\ActivityQuery;
 use App\Repository\Query\ProjectQuery;
 use App\Repository\TeamRepository;
 use App\Utils\Context;
 use App\Utils\DataTable;
 use App\Utils\PageSetup;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\ExpressionLanguage\Expression;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
diff --git a/src/Controller/Reporting/ReportUsersMonthController.php b/src/Controller/Reporting/ReportUsersMonthController.php
index 16f4e0d3..087e4231 100644
--- a/src/Controller/Reporting/ReportUsersMonthController.php
+++ b/src/Controller/Reporting/ReportUsersMonthController.php
@@ -40,11 +40,11 @@ final class ReportUsersMonthController extends AbstractController
     #[Route(path: '/month_export', name: 'report_monthly_users_export', methods: ['GET', 'POST'])]
     public function export(Request $request, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
     {
         $data = $this->getData($request, $statisticService, $userRepository);
 
-        $content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
+        $content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
 
         $reader = new Html();
         $spreadsheet = $reader->loadFromString($content);
 
         $writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-users-monthly');
diff --git a/src/Controller/Reporting/ReportUsersWeekController.php b/src/Controller/Reporting/ReportUsersWeekController.php
index f4c7bcba..4b20c7d9 100644
--- a/src/Controller/Reporting/ReportUsersWeekController.php
+++ b/src/Controller/Reporting/ReportUsersWeekController.php
@@ -40,11 +40,11 @@ final class ReportUsersWeekController extends AbstractController
     #[Route(path: '/week_export', name: 'report_weekly_users_export', methods: ['GET', 'POST'])]
     public function export(Request $request, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
     {
         $data = $this->getData($request, $statisticService, $userRepository);
 
-        $content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
+        $content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
 
         $reader = new Html();
         $spreadsheet = $reader->loadFromString($content);
 
         $writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-users-weekly');
diff --git a/src/Controller/Reporting/ReportUsersYearController.php b/src/Controller/Reporting/ReportUsersYearController.php
index 38671691..88e308a2 100644
--- a/src/Controller/Reporting/ReportUsersYearController.php
+++ b/src/Controller/Reporting/ReportUsersYearController.php
@@ -17,46 +17,35 @@ use App\Model\MonthlyStatistic;
 use App\Reporting\YearlyUserList\YearlyUserList;
 use App\Reporting\YearlyUserList\YearlyUserListForm;
 use App\Repository\Query\UserQuery;
 use App\Repository\UserRepository;
 use App\Timesheet\TimesheetStatisticService;
-use Exception;
 use PhpOffice\PhpSpreadsheet\Reader\Html;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 
 #[Route(path: '/reporting/users')]
 #[IsGranted('report:other')]
 final class ReportUsersYearController extends AbstractController
 {
-    /**
-     * @param Request $request
-     * @return Response
-     * @throws Exception
-     */
     #[Route(path: '/year', name: 'report_yearly_users', methods: ['GET', 'POST'])]
     public function report(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
     {
         return $this->render(
             'reporting/report_user_list_monthly.html.twig',
             $this->getData($request, $systemConfiguration, $statisticService, $userRepository)
         );
     }
 
-    /**
-     * @param Request $request
-     * @return Response
-     * @throws Exception
-     */
     #[Route(path: '/year_export', name: 'report_yearly_users_export', methods: ['GET', 'POST'])]
     public function export(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
     {
         $data = $this->getData($request, $systemConfiguration, $statisticService, $userRepository);
 
-        $content = $this->container->get('twig')->render('reporting/report_user_list_monthly_export.html.twig', $data);
+        $content = $this->renderView('reporting/report_user_list_monthly_export.html.twig', $data);
 
         $reader = new Html();
         $spreadsheet = $reader->loadFromString($content);
 
         $writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-users-yearly');
diff --git a/src/Controller/Reporting/UserMonthController.php b/src/Controller/Reporting/UserMonthController.php
index 8799113a..93bf7755 100644
--- a/src/Controller/Reporting/UserMonthController.php
+++ b/src/Controller/Reporting/UserMonthController.php
@@ -8,14 +8,17 @@
  */
 
 namespace App\Controller\Reporting;
 
 use App\Entity\User;
+use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
+use App\Export\Spreadsheet\Writer\XlsxWriter;
 use App\Model\DailyStatistic;
 use App\Reporting\MonthByUser\MonthByUser;
 use App\Reporting\MonthByUser\MonthByUserForm;
 use Exception;
+use PhpOffice\PhpSpreadsheet\Reader\Html;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -33,10 +36,25 @@ final class UserMonthController extends AbstractUserReportController
     public function monthByUser(Request $request): Response
     {
         return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
     }
 
+    #[Route(path: '/month_export', name: 'report_user_month_export', methods: ['GET', 'POST'])]
+    public function export(Request $request): Response
+    {
+        $data = $this->getData($request);
+
+        $content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
+
+        $reader = new Html();
+        $spreadsheet = $reader->loadFromString($content);
+
+        $writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-monthly');
+
+        return $writer->getFileResponse($spreadsheet);
+    }
+
     private function getData(Request $request): array
     {
         $currentUser = $this->getUser();
         $dateTimeFactory = $this->getDateTimeFactory($currentUser);
         $canChangeUser = $this->canSelectUser();
@@ -93,8 +111,9 @@ final class UserMonthController extends AbstractUserReportController
             'period' => new DailyStatistic($start, $end, $selectedUser),
             'user' => $selectedUser,
             'current' => $start,
             'next' => $nextMonth,
             'previous' => $previousMonth,
+            'export_route' => 'report_user_month_export',
         ];
     }
 }
diff --git a/src/Controller/Reporting/UserWeekController.php b/src/Controller/Reporting/UserWeekController.php
index 1fd5c91a..a0b2b7d3 100644
--- a/src/Controller/Reporting/UserWeekController.php
+++ b/src/Controller/Reporting/UserWeekController.php
@@ -7,14 +7,17 @@
  * file that was distributed with this source code.
  */
 
 namespace App\Controller\Reporting;
 
+use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
+use App\Export\Spreadsheet\Writer\XlsxWriter;
 use App\Model\DailyStatistic;
 use App\Reporting\WeekByUser\WeekByUser;
 use App\Reporting\WeekByUser\WeekByUserForm;
 use Exception;
+use PhpOffice\PhpSpreadsheet\Reader\Html;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -32,10 +35,25 @@ final class UserWeekController extends AbstractUserReportController
     public function weekByUser(Request $request): Response
     {
         return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
     }
 
+    #[Route(path: '/week_export', name: 'report_user_week_export', methods: ['GET', 'POST'])]
+    public function export(Request $request): Response
+    {
+        $data = $this->getData($request);
+
+        $content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
+
+        $reader = new Html();
+        $spreadsheet = $reader->loadFromString($content);
+
+        $writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-weekly');
+
+        return $writer->getFileResponse($spreadsheet);
+    }
+
     private function getData(Request $request): array
     {
         $currentUser = $this->getUser();
         $dateTimeFactory = $this->getDateTimeFactory($currentUser);
         $canChangeUser = $this->canSelectUser();
@@ -86,8 +104,9 @@ final class UserWeekController extends AbstractUserReportController
             'rows' => $data,
             'user' => $selectedUser,
             'current' => $start,
             'next' => $next,
             'previous' => $previous,
+            'export_route' => 'report_user_week_export',
         ];
     }
 }
diff --git a/src/Controller/Reporting/UserYearController.php b/src/Controller/Reporting/UserYearController.php
index cecee575..a4104af7 100644
--- a/src/Controller/Reporting/UserYearController.php
+++ b/src/Controller/Reporting/UserYearController.php
@@ -9,17 +9,20 @@
 
 namespace App\Controller\Reporting;
 
 use App\Configuration\SystemConfiguration;
 use App\Entity\User;
+use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
+use App\Export\Spreadsheet\Writer\XlsxWriter;
 use App\Model\DateStatisticInterface;
 use App\Model\MonthlyStatistic;
 use App\Reporting\YearByUser\YearByUser;
 use App\Reporting\YearByUser\YearByUserForm;
 use DateTime;
 use DateTimeInterface;
 use Exception;
+use PhpOffice\PhpSpreadsheet\Reader\Html;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -37,10 +40,25 @@ final class UserYearController extends AbstractUserReportController
     public function yearByUser(Request $request, SystemConfiguration $systemConfiguration): Response
     {
         return $this->render('reporting/report_by_user_year.html.twig', $this->getData($request, $systemConfiguration));
     }
 
+    #[Route(path: '/year_export', name: 'report_user_year_export', methods: ['GET', 'POST'])]
+    public function export(Request $request, SystemConfiguration $systemConfiguration): Response
+    {
+        $data = $this->getData($request, $systemConfiguration);
+
+        $content = $this->renderView('reporting/report_by_user_year_export.html.twig', $data);
+
+        $reader = new Html();
+        $spreadsheet = $reader->loadFromString($content);
+
+        $writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-yearly');
+
+        return $writer->getFileResponse($spreadsheet);
+    }
+
     private function getData(Request $request, SystemConfiguration $systemConfiguration): array
     {
         $currentUser = $this->getUser();
         $dateTimeFactory = $this->getDateTimeFactory($currentUser);
         $canChangeUser = $this->canSelectUser();
@@ -102,10 +120,11 @@ final class UserYearController extends AbstractUserReportController
             'rows' => $data,
             'user' => $selectedUser,
             'current' => $start,
             'next' => $next,
             'previous' => $previous,
+            'export_route' => 'report_user_year_export',
         ];
     }
 
     protected function getStatisticDataRaw(DateTimeInterface $begin, DateTimeInterface $end, User $user): array
     {
diff --git a/src/Controller/Security/LoginLinkController.php b/src/Controller/Security/LoginLinkController.php
new file mode 100644
index 00000000..4d91644f
--- /dev/null
+++ b/src/Controller/Security/LoginLinkController.php
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the Kimai time-tracking app.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Controller\Security;
+
+use App\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Annotation\Route;
+
+#[Route(path: '/auth/link')]
+/**
+ * @CloudRequired
+ */
+final class LoginLinkController extends AbstractController
+{
+    #[Route(path: '/check', name: 'link_login_check', methods: ['GET'])]
+    public function check(): Response
+    {
+        return new Response();
+    }
+}
diff --git a/src/Controller/Security/PasswordResetController.php b/src/Controller/Security/PasswordResetController.php
index e74809e0..b7a12f7b 100644
--- a/src/Controller/Security/PasswordResetController.php
+++ b/src/Controller/Security/PasswordResetController.php
@@ -16,19 +16,19 @@ use App\Event\EmailEvent;
 use App\Event\EmailPasswordResetEvent;
 use App\Form\PasswordResetForm;
 use App\User\LoginManager;
 use App\User\UserService;
 use DateTime;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Mime\Address;
 use Symfony\Component\Mime\Email;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
 #[Route(path: '/resetting')]
 final class PasswordResetController extends AbstractController
 {
@@ -106,11 +106,11 @@ final class PasswordResetController extends AbstractController
             // the user does not come from the sendEmail action
             return $this->redirectToRoute('resetting_request');
         }
 
         return $this->render('security/password-reset/check_email.html.twig', [
-            'tokenLifetime' => ceil($this->configuration->getPasswordResetRetryLifetime() / 3600),
+            'tokenLifetime' => $this->configuration->getPasswordResetRetryLifetime(),
         ]);
     }
 
     /**
      * Reset user password.
diff --git a/src/Controller/Security/SelfRegistrationController.php b/src/Controller/Security/SelfRegistrationController.php
index 6a78f268..073c4d70 100644
--- a/src/Controller/Security/SelfRegistrationController.php
+++ b/src/Controller/Security/SelfRegistrationController.php
@@ -15,21 +15,21 @@ use App\Entity\User;
 use App\Event\EmailEvent;
 use App\Event\EmailSelfRegistrationEvent;
 use App\Form\SelfRegistrationForm;
 use App\User\LoginManager;
 use App\User\UserService;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\Session\SessionInterface;
 use Symfony\Component\Mime\Address;
 use Symfony\Component\Mime\Email;
 use Symfony\Component\Routing\Annotation\Route;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
 #[Route(path: '/register')]
 final class SelfRegistrationController extends AbstractController
 {
diff --git a/src/Controller/SystemConfigurationController.php b/src/Controller/SystemConfigurationController.php
index 05c4c693..56c02405 100644
--- a/src/Controller/SystemConfigurationController.php
+++ b/src/Controller/SystemConfigurationController.php
@@ -34,11 +34,11 @@ use App\Form\Type\YesNoType;
 use App\Timesheet\LockdownService;
 use App\Utils\PageSetup;
 use App\Validator\Constraints\ColorChoices;
 use App\Validator\Constraints\DateTimeFormat;
 use App\Validator\Constraints\TimeFormat;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Form\Extension\Core\Type\CountryType;
 use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
 use Symfony\Component\Form\Extension\Core\Type\IntegerType;
 use Symfony\Component\Form\Extension\Core\Type\TextType;
 use Symfony\Component\Form\FormInterface;
diff --git a/src/Controller/TimesheetAbstractController.php b/src/Controller/TimesheetAbstractController.php
index 0433e7cb..82b818ec 100644
--- a/src/Controller/TimesheetAbstractController.php
+++ b/src/Controller/TimesheetAbstractController.php
@@ -31,11 +31,11 @@ use App\Repository\TagRepository;
 use App\Repository\TimesheetRepository;
 use App\Timesheet\TimesheetService;
 use App\Timesheet\TrackingMode\TrackingModeInterface;
 use App\Utils\DataTable;
 use App\Utils\PageSetup;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Form\FormError;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -332,10 +332,17 @@ abstract class TimesheetAbstractController extends AbstractController
 
         if ($form->isSubmitted() && $form->isValid()) {
             $execute = false;
             /** @var Timesheet $timesheet */
             foreach ($timesheets as $timesheet) {
+                if ($dto->isReplaceDescription()) {
+                    $timesheet->setDescription($dto->getDescription());
+                    $execute = true;
+                } elseif($dto->getDescription() !== null && $dto->getDescription() !== '') {
+                    $timesheet->setDescription($timesheet->getDescription() . PHP_EOL . $dto->getDescription());
+                    $execute = true;
+                }
                 if ($dto->isReplaceTags()) {
                     foreach ($timesheet->getTags() as $tag) {
                         $timesheet->removeTag($tag);
                     }
                     $execute = true;
diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php
index bd0f5785..56c305ac 100644
--- a/src/Controller/UserController.php
+++ b/src/Controller/UserController.php
@@ -23,11 +23,11 @@ use App\Repository\Query\UserQuery;
 use App\Repository\TimesheetRepository;
 use App\Repository\UserRepository;
 use App\User\UserService;
 use App\Utils\DataTable;
 use App\Utils\PageSetup;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
 use Symfony\Component\Routing\Annotation\Route;
diff --git a/src/Controller/WizardController.php b/src/Controller/WizardController.php
index 9e944ea3..d93277c5 100644
--- a/src/Controller/WizardController.php
+++ b/src/Controller/WizardController.php
@@ -12,10 +12,11 @@ namespace App\Controller;
 use App\Entity\User;
 use App\Entity\UserPreference;
 use App\Form\Type\LanguageType;
 use App\Form\Type\SkinType;
 use App\Form\Type\TimezoneType;
+use App\Form\UserPasswordType;
 use App\User\UserService;
 use Symfony\Component\Form\Extension\Core\Type\HiddenType;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
@@ -56,10 +57,15 @@ final class WizardController extends AbstractController
                 ->add('reload', HiddenType::class)
                 ->setAction($this->generateUrl('wizard', ['wizard' => 'profile']))
                 ->setMethod('POST')
                 ->getForm();
 
+            $next = 'done';
+            if ($user->requiresPasswordReset()) {
+                $next = 'password';
+            }
+
             $form->handleRequest($request);
 
             if ($form->isSubmitted() && $form->isValid()) {
                 /** @var array<string, string> $data */
                 $data = $form->getData();
@@ -70,17 +76,48 @@ final class WizardController extends AbstractController
                 $userService->updateUser($user);
 
                 if ($data['reload'] === '1') {
                     return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $data['language']]);
                 } else {
-                    return $this->redirectToRoute('wizard', ['wizard' => 'done', '_locale' => $data['language']]);
+                    return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $data['language']]);
                 }
             }
 
             return $this->render('wizard/profile.html.twig', [
                 'percent' => \intval(100 / \count(User::WIZARDS) * 1),
                 'previous' => 'intro',
+                'next' => $next,
+                'form' => $form->createView(),
+            ]);
+        }
+
+        if ($wizard === 'password' || $user->requiresPasswordReset()) {
+            $form = $this->createForm(UserPasswordType::class, $user, [
+                'action' => $this->generateUrl('wizard', ['wizard' => 'password']),
+                'method' => 'POST',
+            ]);
+
+            $form->handleRequest($request);
+
+            if ($form->isSubmitted() && $form->isValid()) {
+                $user->setRequiresPasswordReset(false);
+                $userService->updateUser($user);
+
+                return $this->redirectToRoute('wizard', ['wizard' => 'done']);
+            }
+
+            $previous = 'profile';
+            $percent = \intval(100 / \count(User::WIZARDS) * 1);
+
+            if ($user->requiresPasswordReset()) {
+                $previous = null;
+                $percent = null;
+            }
+
+            return $this->render('wizard/password.html.twig', [
+                'percent' => $percent,
+                'previous' => $previous,
                 'next' => 'done',
                 'form' => $form->createView(),
             ]);
         }
 
diff --git a/src/Customer/CustomerService.php b/src/Customer/CustomerService.php
index 167810bb..5ee56cd8 100644
--- a/src/Customer/CustomerService.php
+++ b/src/Customer/CustomerService.php
@@ -20,11 +20,11 @@ use App\Event\CustomerUpdatePreEvent;
 use App\Repository\CustomerRepository;
 use App\Repository\Query\CustomerQuery;
 use App\Utils\NumberGenerator;
 use App\Validator\ValidationFailedException;
 use InvalidArgumentException;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Validator\Validator\ValidatorInterface;
 
 final class CustomerService
 {
     public function __construct(
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 6ea601e2..a87d7770 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -341,11 +341,11 @@ final class Configuration implements ConfigurationInterface
                 ->end()
                 ->scalarNode('number_format')
                     ->defaultValue('{Y}/{cy,3}')
                 ->end()
                 ->booleanNode('upload_twig')
-                    ->defaultTrue()
+                    ->defaultFalse()
                 ->end()
             ->end()
         ;
 
         return $node;
diff --git a/src/Doctrine/ModifiedSubscriber.php b/src/Doctrine/ModifiedSubscriber.php
new file mode 100644
index 00000000..e8218b51
--- /dev/null
+++ b/src/Doctrine/ModifiedSubscriber.php
@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * This file is part of the Kimai time-tracking app.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Doctrine;
+
+use App\Entity\Timesheet;
+use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
+use Doctrine\Common\EventSubscriber;
+use Doctrine\ORM\Event\OnFlushEventArgs;
+use Doctrine\ORM\Events;
+
+/**
+ * Automatically set the modifiedAt field for all Timesheet entries.
+ */
+#[AsDoctrineListener(event: Events::onFlush, priority: 60)]
+final class ModifiedSubscriber implements EventSubscriber, DataSubscriberInterface
+{
+    public function getSubscribedEvents(): array
+    {
+        return [
+            Events::onFlush,
+        ];
+    }
+
+    public function onFlush(OnFlushEventArgs $args): void
+    {
+        $uow = $args->getObjectManager()->getUnitOfWork();
+        $now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
+
+        foreach ($uow->getScheduledEntityUpdates() as $entity) {
+            if (!($entity instanceof Timesheet)) {
+                continue;
+            }
+            $entity->setModifiedAt($now);
+        }
+
+        foreach ($uow->getScheduledEntityInsertions() as $entity) {
+            if (!($entity instanceof Timesheet)) {
+                continue;
+            }
+            $entity->setModifiedAt($now);
+        }
+    }
+}
diff --git a/src/Doctrine/TimesheetSubscriber.php b/src/Doctrine/TimesheetSubscriber.php
index 02a8c799..44eda77b 100644
--- a/src/Doctrine/TimesheetSubscriber.php
+++ b/src/Doctrine/TimesheetSubscriber.php
@@ -9,17 +9,19 @@
 
 namespace App\Doctrine;
 
 use App\Entity\Timesheet;
 use App\Timesheet\CalculatorInterface;
+use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
 use Doctrine\Common\EventSubscriber;
 use Doctrine\ORM\Event\OnFlushEventArgs;
 use Doctrine\ORM\Events;
 
 /**
  * A listener to make sure all Timesheet entries will be calculated properly (e.g. duration and rates).
  */
+#[AsDoctrineListener(event: Events::onFlush, priority: 50)]
 final class TimesheetSubscriber implements EventSubscriber, DataSubscriberInterface
 {
     /**
      * @var CalculatorInterface[]
      */
diff --git a/src/Entity/Activity.php b/src/Entity/Activity.php
index 1ac97f02..11451cf7 100644
--- a/src/Entity/Activity.php
+++ b/src/Entity/Activity.php
@@ -42,11 +42,11 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
     #[Serializer\Expose]
     #[Serializer\Groups(['Default'])]
     #[Exporter\Expose(label: 'id', type: 'integer')]
     private ?int $id = null;
     #[ORM\ManyToOne(targetEntity: Project::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
+    #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
     #[Serializer\Expose]
     #[Serializer\Groups(['Subresource', 'Expanded'])]
     #[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
     private ?Project $project = null;
     /**
@@ -85,11 +85,11 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
     /**
      * Meta fields registered with the activity
      *
      * @var Collection<ActivityMeta>
      */
-    #[ORM\OneToMany(targetEntity: ActivityMeta::class, mappedBy: 'activity', cascade: ['persist'])]
+    #[ORM\OneToMany(mappedBy: 'activity', targetEntity: ActivityMeta::class, cascade: ['persist'])]
     #[Serializer\Expose]
     #[Serializer\Groups(['Activity'])]
     #[Serializer\Type(name: 'array<App\Entity\ActivityMeta>')]
     #[Serializer\SerializedName('metaFields')]
     #[Serializer\Accessor(getter: 'getVisibleMetaFields')]
@@ -100,11 +100,11 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
      * @var Collection<Team>
      */
     #[ORM\JoinTable(name: 'kimai2_activities_teams')]
     #[ORM\JoinColumn(name: 'activity_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
     #[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
-    #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'activities')]
+    #[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'activities', cascade: ['persist'])]
     #[Serializer\Expose]
     #[Serializer\Groups(['Activity'])]
     #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
     private Collection $teams;
     #[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
diff --git a/src/Entity/ActivityMeta.php b/src/Entity/ActivityMeta.php
index f4f1561a..93c12943 100644
--- a/src/Entity/ActivityMeta.php
+++ b/src/Entity/ActivityMeta.php
@@ -21,11 +21,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class ActivityMeta implements MetaTableTypeInterface
 {
     use MetaTableTypeTrait;
 
     #[ORM\ManyToOne(targetEntity: Activity::class, inversedBy: 'meta')]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Activity $activity = null;
 
     public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
     {
diff --git a/src/Entity/ActivityRate.php b/src/Entity/ActivityRate.php
index ff4de09b..59156066 100644
--- a/src/Entity/ActivityRate.php
+++ b/src/Entity/ActivityRate.php
@@ -23,11 +23,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class ActivityRate implements RateInterface
 {
     use Rate;
 
     #[ORM\ManyToOne(targetEntity: Activity::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Activity $activity = null;
 
     public function setActivity(?Activity $activity): ActivityRate
     {
diff --git a/src/Entity/Bookmark.php b/src/Entity/Bookmark.php
index 25af24a7..82d3236d 100644
--- a/src/Entity/Bookmark.php
+++ b/src/Entity/Bookmark.php
@@ -27,11 +27,11 @@ class Bookmark
     #[ORM\Column(name: 'id', type: 'integer')]
     #[ORM\Id]
     #[ORM\GeneratedValue(strategy: 'IDENTITY')]
     private ?int $id = null;
     #[ORM\ManyToOne(targetEntity: User::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?User $user = null;
     #[ORM\Column(name: 'type', type: 'string', length: 20, nullable: false)]
     #[Assert\NotBlank]
     #[Assert\Length(min: 2, max: 20)]
diff --git a/src/Entity/CommentTableTypeTrait.php b/src/Entity/CommentTableTypeTrait.php
index 314e4b86..8a1b51d8 100644
--- a/src/Entity/CommentTableTypeTrait.php
+++ b/src/Entity/CommentTableTypeTrait.php
@@ -20,11 +20,11 @@ trait CommentTableTypeTrait
     private ?int $id = null;
     #[ORM\Column(name: 'message', type: 'text', nullable: false)]
     #[Assert\NotNull]
     private ?string $message = null;
     #[ORM\ManyToOne(targetEntity: User::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?User $createdBy = null;
     #[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
     #[Assert\NotNull]
     private ?\DateTime $createdAt = null;
diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php
index 299c1042..efa866dd 100644
--- a/src/Entity/Customer.php
+++ b/src/Entity/Customer.php
@@ -154,11 +154,11 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
     /**
      * Meta fields registered with the customer
      *
      * @var Collection<CustomerMeta>
      */
-    #[ORM\OneToMany(targetEntity: CustomerMeta::class, mappedBy: 'customer', cascade: ['persist'])]
+    #[ORM\OneToMany(mappedBy: 'customer', targetEntity: CustomerMeta::class, cascade: ['persist'])]
     #[Serializer\Expose]
     #[Serializer\Groups(['Customer'])]
     #[Serializer\Type(name: 'array<App\Entity\CustomerMeta>')]
     #[Serializer\SerializedName('metaFields')]
     #[Serializer\Accessor(getter: 'getVisibleMetaFields')]
@@ -169,20 +169,20 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
      * @var Collection<Team>
      */
     #[ORM\JoinTable(name: 'kimai2_customers_teams')]
     #[ORM\JoinColumn(name: 'customer_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
     #[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
-    #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'customers')]
+    #[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'customers', cascade: ['persist'])]
     #[Serializer\Expose]
     #[Serializer\Groups(['Customer'])]
     #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
     private Collection $teams;
     /**
      * Default invoice template for this customer
      */
     #[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)]
-    #[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
+    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
     private ?InvoiceTemplate $invoiceTemplate = null;
     #[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
     private ?string $invoiceText = null;
 
     public function __construct(string $name)
diff --git a/src/Entity/CustomerComment.php b/src/Entity/CustomerComment.php
index 012e9770..415e22b5 100644
--- a/src/Entity/CustomerComment.php
+++ b/src/Entity/CustomerComment.php
@@ -19,11 +19,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class CustomerComment implements CommentInterface
 {
     use CommentTableTypeTrait;
 
     #[ORM\ManyToOne(targetEntity: Customer::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private Customer $customer;
 
     public function __construct(Customer $customer)
     {
diff --git a/src/Entity/CustomerMeta.php b/src/Entity/CustomerMeta.php
index f396e82a..828e1b97 100644
--- a/src/Entity/CustomerMeta.php
+++ b/src/Entity/CustomerMeta.php
@@ -21,11 +21,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class CustomerMeta implements MetaTableTypeInterface
 {
     use MetaTableTypeTrait;
 
     #[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'meta')]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Customer $customer = null;
 
     public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
     {
diff --git a/src/Entity/CustomerRate.php b/src/Entity/CustomerRate.php
index 4d747459..9ab493f4 100644
--- a/src/Entity/CustomerRate.php
+++ b/src/Entity/CustomerRate.php
@@ -23,11 +23,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class CustomerRate implements RateInterface
 {
     use Rate;
 
     #[ORM\ManyToOne(targetEntity: Customer::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Customer $customer = null;
 
     public function setCustomer(?Customer $customer): CustomerRate
     {
diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php
index c5188b0e..30ae0f5e 100644
--- a/src/Entity/Invoice.php
+++ b/src/Entity/Invoice.php
@@ -54,15 +54,15 @@ class Invoice implements EntityWithMetaFields
     #[Serializer\Expose]
     #[Serializer\Groups(['Customer_Entity'])]
     #[Exporter\Expose(label: 'comment')]
     private ?string $comment = null;
     #[ORM\ManyToOne(targetEntity: Customer::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Customer $customer = null;
     #[ORM\ManyToOne(targetEntity: User::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?User $user = null;
     #[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
     #[Assert\NotNull]
     #[Exporter\Expose(label: 'date', type: 'datetime')]
@@ -107,11 +107,11 @@ class Invoice implements EntityWithMetaFields
     /**
      * Meta fields registered with the invoice
      *
      * @var Collection<InvoiceMeta>
      */
-    #[ORM\OneToMany(targetEntity: InvoiceMeta::class, mappedBy: 'invoice', cascade: ['persist'])]
+    #[ORM\OneToMany(mappedBy: 'invoice', targetEntity: InvoiceMeta::class, cascade: ['persist'])]
     #[Serializer\Expose]
     #[Serializer\Groups(['Invoice'])]
     #[Serializer\Type(name: 'array<App\Entity\InvoiceMeta>')]
     #[Serializer\SerializedName('metaFields')]
     #[Serializer\Accessor(getter: 'getVisibleMetaFields')]
diff --git a/src/Entity/InvoiceMeta.php b/src/Entity/InvoiceMeta.php
index d41db1e9..01a837d9 100644
--- a/src/Entity/InvoiceMeta.php
+++ b/src/Entity/InvoiceMeta.php
@@ -21,11 +21,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class InvoiceMeta implements MetaTableTypeInterface
 {
     use MetaTableTypeTrait;
 
     #[ORM\ManyToOne(targetEntity: Invoice::class, inversedBy: 'meta')]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Invoice $invoice = null;
 
     public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
     {
diff --git a/src/Entity/Project.php b/src/Entity/Project.php
index fbbdc1f6..c40fc64b 100644
--- a/src/Entity/Project.php
+++ b/src/Entity/Project.php
@@ -46,11 +46,11 @@ class Project implements EntityWithMetaFields, EntityWithBudget
     private ?int $id = null;
     /**
      * Customer for this project
      */
     #[ORM\ManyToOne(targetEntity: Customer::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     #[Serializer\Expose]
     #[Serializer\Groups(['Subresource', 'Expanded'])]
     #[OA\Property(ref: '#/components/schemas/Customer')]
     private ?Customer $customer = null;
@@ -135,11 +135,11 @@ class Project implements EntityWithMetaFields, EntityWithBudget
     /**
      * Meta fields registered with the project
      *
      * @var Collection<ProjectMeta>
      */
-    #[ORM\OneToMany(targetEntity: ProjectMeta::class, mappedBy: 'project', cascade: ['persist'])]
+    #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectMeta::class, cascade: ['persist'])]
     #[Serializer\Expose]
     #[Serializer\Groups(['Project'])]
     #[Serializer\Type(name: 'array<App\Entity\ProjectMeta>')]
     #[Serializer\SerializedName('metaFields')]
     #[Serializer\Accessor(getter: 'getVisibleMetaFields')]
@@ -150,11 +150,11 @@ class Project implements EntityWithMetaFields, EntityWithBudget
      * @var Collection<Team>
      */
     #[ORM\JoinTable(name: 'kimai2_projects_teams')]
     #[ORM\JoinColumn(name: 'project_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
     #[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
-    #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'projects')]
+    #[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'projects', cascade: ['persist'])]
     #[Serializer\Expose]
     #[Serializer\Groups(['Project'])]
     #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
     private Collection $teams;
     #[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
diff --git a/src/Entity/ProjectComment.php b/src/Entity/ProjectComment.php
index ddba3cd2..b27e6a35 100644
--- a/src/Entity/ProjectComment.php
+++ b/src/Entity/ProjectComment.php
@@ -19,11 +19,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class ProjectComment implements CommentInterface
 {
     use CommentTableTypeTrait;
 
     #[ORM\ManyToOne(targetEntity: Project::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private Project $project;
 
     public function __construct(Project $project)
     {
diff --git a/src/Entity/ProjectMeta.php b/src/Entity/ProjectMeta.php
index 89e7fac2..5c5dcd19 100644
--- a/src/Entity/ProjectMeta.php
+++ b/src/Entity/ProjectMeta.php
@@ -21,11 +21,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class ProjectMeta implements MetaTableTypeInterface
 {
     use MetaTableTypeTrait;
 
     #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'meta')]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Project $project = null;
 
     public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
     {
diff --git a/src/Entity/ProjectRate.php b/src/Entity/ProjectRate.php
index 7bdc8695..54095fe3 100644
--- a/src/Entity/ProjectRate.php
+++ b/src/Entity/ProjectRate.php
@@ -23,11 +23,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class ProjectRate implements RateInterface
 {
     use Rate;
 
     #[ORM\ManyToOne(targetEntity: Project::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Project $project = null;
 
     public function setProject(?Project $project): ProjectRate
     {
diff --git a/src/Entity/Rate.php b/src/Entity/Rate.php
index 15e1c31b..cb7b71ce 100644
--- a/src/Entity/Rate.php
+++ b/src/Entity/Rate.php
@@ -21,11 +21,11 @@ trait Rate
     #[ORM\GeneratedValue(strategy: 'IDENTITY')]
     #[Serializer\Expose]
     #[Serializer\Groups(['Default'])]
     private ?int $id = null;
     #[ORM\ManyToOne(targetEntity: User::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
+    #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
     #[Serializer\Expose]
     #[Serializer\Groups(['Default'])]
     #[OA\Property(ref: '#/components/schemas/User')]
     private ?User $user = null;
     #[ORM\Column(name: 'rate', type: 'float', nullable: false)]
diff --git a/src/Entity/RolePermission.php b/src/Entity/RolePermission.php
index 5c18e179..b1477acd 100644
--- a/src/Entity/RolePermission.php
+++ b/src/Entity/RolePermission.php
@@ -23,11 +23,11 @@ class RolePermission
     #[ORM\Column(name: 'id', type: 'integer')]
     #[ORM\Id]
     #[ORM\GeneratedValue(strategy: 'IDENTITY')]
     private ?int $id = null;
     #[ORM\ManyToOne(targetEntity: Role::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Role $role = null;
     #[ORM\Column(name: 'permission', type: 'string', length: 50, nullable: false)]
     #[Assert\Length(max: 50)]
     private ?string $permission = null;
diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php
index febc8546..dc95b04f 100644
--- a/src/Entity/Tag.php
+++ b/src/Entity/Tag.php
@@ -37,11 +37,11 @@ class Tag
      * The tag name
      */
     #[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
     #[Assert\NotBlank]
     #[Assert\Length(min: 2, max: 100, normalizer: 'trim')]
-    #[Assert\Regex(pattern: '/,/', match: false, message: 'Tag name cannot contain comma')]
+    #[Assert\Regex(pattern: '/,/', message: 'Tag name cannot contain comma', match: false)]
     #[Serializer\Expose]
     #[Serializer\Groups(['Default'])]
     private ?string $name = null;
     #[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => true])]
     #[Assert\NotNull]
diff --git a/src/Entity/Team.php b/src/Entity/Team.php
index e6404fd9..222e9f98 100644
--- a/src/Entity/Team.php
+++ b/src/Entity/Team.php
@@ -45,11 +45,11 @@ class Team
     /**
      * All team member (including team leads)
      *
      * @var Collection<TeamMember>
      */
-    #[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'team', fetch: 'LAZY', cascade: ['persist', 'remove'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'team', targetEntity: TeamMember::class, cascade: ['persist', 'remove'], fetch: 'LAZY', orphanRemoval: true)]
     #[ORM\JoinColumn(onDelete: 'CASCADE')]
     #[Assert\Count(min: 1)]
     #[Serializer\Expose]
     #[Serializer\Groups(['Team_Entity'])]
     #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamMember'))]
@@ -57,31 +57,31 @@ class Team
     /**
      * Customers assigned to the team
      *
      * @var Collection<Customer>
      */
-    #[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
+    #[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
     #[Serializer\Expose]
     #[Serializer\Groups(['Team_Entity'])]
     #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Customer'))]
     private Collection $customers;
     /**
      * Projects assigned to the team
      *
      * @var Collection<Project>
      */
-    #[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
+    #[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
     #[Serializer\Expose]
     #[Serializer\Groups(['Team_Entity', 'Expanded'])]
     #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Project'))]
     private Collection $projects;
     /**
      * Activities assigned to the team
      *
      * @var Collection<Activity>
      */
-    #[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
+    #[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
     #[Serializer\Expose]
     #[Serializer\Groups(['Team_Entity', 'Expanded'])]
     #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Activity'))]
     private Collection $activities;
 
diff --git a/src/Entity/TeamMember.php b/src/Entity/TeamMember.php
index 946ceb14..2aed3f3e 100644
--- a/src/Entity/TeamMember.php
+++ b/src/Entity/TeamMember.php
@@ -24,18 +24,18 @@ class TeamMember
     #[ORM\Column(name: 'id', type: 'integer')]
     #[ORM\Id]
     #[ORM\GeneratedValue(strategy: 'IDENTITY')]
     private ?int $id = null;
     #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'memberships')]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     #[Serializer\Expose]
     #[Serializer\Groups(['Default', 'Entity', 'Team_Entity'])]
     #[OA\Property(ref: '#/components/schemas/User')]
     private ?User $user = null;
     #[ORM\ManyToOne(targetEntity: Team::class, inversedBy: 'members')]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     #[Serializer\Expose]
     #[Serializer\Groups(['Default', 'Entity', 'User_Entity'])]
     #[OA\Property(ref: '#/components/schemas/Team')]
     private ?Team $team = null;
diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php
index 253c1f3d..ba2feb65 100644
--- a/src/Entity/Timesheet.php
+++ b/src/Entity/Timesheet.php
@@ -13,11 +13,10 @@ use App\Validator\Constraints as Constraints;
 use DateTime;
 use DateTimeZone;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
-use Gedmo\Mapping\Annotation as Gedmo;
 use JMS\Serializer\Annotation as Serializer;
 use OpenApi\Attributes as OA;
 use Symfony\Component\Validator\Constraints as Assert;
 
 /**
@@ -126,25 +125,25 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
     #[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
     #[Serializer\Expose]
     #[Serializer\Groups(['Default'])]
     private ?int $duration = 0;
     #[ORM\ManyToOne(targetEntity: User::class)]
-    #[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     #[Serializer\Expose]
     #[Serializer\Groups(['Subresource', 'Expanded'])]
     #[OA\Property(ref: '#/components/schemas/User')]
     private ?User $user = null;
     #[ORM\ManyToOne(targetEntity: Activity::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     #[Serializer\Expose]
     #[Serializer\Groups(['Subresource', 'Expanded'])]
     #[OA\Property(ref: '#/components/schemas/ActivityExpanded')]
     private ?Activity $activity = null;
     #[ORM\ManyToOne(targetEntity: Project::class)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     #[Serializer\Expose]
     #[Serializer\Groups(['Subresource', 'Expanded'])]
     #[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
     private ?Project $project = null;
@@ -187,16 +186,12 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
     #[Assert\NotNull]
     private ?string $billableMode = self::BILLABLE_DEFAULT;
     #[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
     #[Assert\NotNull]
     private ?string $category = self::WORK;
-    /**
-     * @internal used for limiting queries, eg. via API sync
-     */
     #[ORM\Column(name: 'modified_at', type: 'datetime', nullable: true)]
-    #[Gedmo\Timestampable]
-    private ?\DateTime $modifiedAt = null;
+    private \DateTimeInterface $modifiedAt;
     /**
      * Tags
      *
      * @var Collection<Tag>
      */
@@ -209,11 +204,11 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
     /**
      * Meta fields registered with the timesheet
      *
      * @var Collection<TimesheetMeta>
      */
-    #[ORM\OneToMany(targetEntity: TimesheetMeta::class, mappedBy: 'timesheet', cascade: ['persist'])]
+    #[ORM\OneToMany(mappedBy: 'timesheet', targetEntity: TimesheetMeta::class, cascade: ['persist'])]
     #[Serializer\Expose]
     #[Serializer\Groups(['Timesheet'])]
     #[Serializer\Type(name: 'array<App\Entity\TimesheetMeta>')]
     #[Serializer\SerializedName('metaFields')]
     #[Serializer\Accessor(getter: 'getVisibleMetaFields')]
@@ -221,10 +216,11 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
 
     public function __construct()
     {
         $this->tags = new ArrayCollection();
         $this->meta = new ArrayCollection();
+        $this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
     }
 
     /**
      * Get entry id, returns null for new entities which were not persisted.
      *
@@ -588,15 +584,20 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
         $this->hourlyRate = $hourlyRate;
 
         return $this;
     }
 
-    public function getModifiedAt(): ?DateTime
+    public function getModifiedAt(): \DateTimeInterface
     {
         return $this->modifiedAt;
     }
 
+    public function setModifiedAt(\DateTimeInterface $dateTime): void
+    {
+        $this->modifiedAt = $dateTime;
+    }
+
     /**
      * @return Collection|MetaTableTypeInterface[]
      */
     public function getMetaFields(): Collection
     {
@@ -685,11 +686,11 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
         if ($this->id) {
             $this->id = null;
         }
 
         // field will not be set, if it contains a value
-        $this->modifiedAt = null;
+        $this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
         $this->exported = false;
 
         $currentMeta = $this->meta;
         $this->meta = new ArrayCollection();
         /** @var TimesheetMeta $meta */
diff --git a/src/Entity/TimesheetMeta.php b/src/Entity/TimesheetMeta.php
index 4e116f64..380bb9a9 100644
--- a/src/Entity/TimesheetMeta.php
+++ b/src/Entity/TimesheetMeta.php
@@ -21,11 +21,11 @@ use Symfony\Component\Validator\Constraints as Assert;
 class TimesheetMeta implements MetaTableTypeInterface
 {
     use MetaTableTypeTrait;
 
     #[ORM\ManyToOne(targetEntity: Timesheet::class, inversedBy: 'meta')]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     private ?Timesheet $timesheet = null;
 
     public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
     {
diff --git a/src/Entity/User.php b/src/Entity/User.php
index 2f25f092..02ffd21d 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -40,13 +40,13 @@ use Symfony\Component\Validator\Constraints as Assert;
 #[Exporter\Order(['id', 'username', 'alias', 'title', 'email', 'last_login', 'language', 'timezone', 'active', 'registeredAt', 'roles', 'teams', 'color', 'accountNumber'])]
 #[Exporter\Expose(name: 'email', label: 'email', exp: 'object.getEmail()')]
 #[Exporter\Expose(name: 'username', label: 'username', exp: 'object.getUserIdentifier()')]
 #[Exporter\Expose(name: 'timezone', label: 'timezone', exp: 'object.getTimezone()')]
 #[Exporter\Expose(name: 'language', label: 'language', exp: 'object.getLanguage()')]
-#[Exporter\Expose(name: 'last_login', label: 'lastLogin', exp: 'object.getLastLogin()', type: 'datetime')]
-#[Exporter\Expose(name: 'roles', label: 'roles', exp: 'object.getRoles()', type: 'array')]
-#[Exporter\Expose(name: 'active', label: 'active', exp: 'object.isEnabled()', type: 'boolean')]
+#[Exporter\Expose(name: 'last_login', label: 'lastLogin', type: 'datetime', exp: 'object.getLastLogin()')]
+#[Exporter\Expose(name: 'roles', label: 'roles', type: 'array', exp: 'object.getRoles()')]
+#[Exporter\Expose(name: 'active', label: 'active', type: 'boolean', exp: 'object.isEnabled()')]
 #[Constraints\User(groups: ['UserCreate', 'Registration', 'Default', 'Profile'])]
 class User implements UserInterface, EquatableInterface, ThemeUserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
 {
     public const ROLE_USER = 'ROLE_USER';
     public const ROLE_TEAMLEAD = 'ROLE_TEAMLEAD';
@@ -125,19 +125,19 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
      * if a currently logged-in user will be deleted and then refreshed from the session from one of the UserProvider
      * e.g. see LdapUserProvider::refreshUser() it might crash if $user->getPreferenceValue() is called
      *
      * @var Collection<UserPreference>|null
      */
-    #[ORM\OneToMany(targetEntity: UserPreference::class, mappedBy: 'user', cascade: ['persist'])]
+    #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserPreference::class, cascade: ['persist'])]
     private ?Collection $preferences = null;
     /**
      * List of all team memberships.
      *
      * @var Collection<TeamMember>
      */
-    #[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'user', fetch: 'LAZY', cascade: ['persist'], orphanRemoval: true)]
-    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
+    #[ORM\OneToMany(mappedBy: 'user', targetEntity: TeamMember::class, cascade: ['persist'], fetch: 'LAZY', orphanRemoval: true)]
+    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
     #[Assert\NotNull]
     #[Serializer\Expose]
     #[Serializer\Groups(['User_Entity'])]
     #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamMembership'))]
     private Collection $memberships;
@@ -215,11 +215,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
     #[ORM\Column(name: 'totp_enabled', type: 'boolean', nullable: false, options: ['default' => false])]
     private bool $totpEnabled = false;
     #[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => false])]
     private bool $systemAccount = false;
     #[ORM\ManyToOne(targetEntity: User::class)]
-    #[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
+    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
     #[Serializer\Expose]
     #[Serializer\Groups(['User_Entity'])]
     #[OA\Property(ref: '#/components/schemas/User')]
     private ?User $supervisor = null;
 
@@ -295,10 +295,18 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
         $this->apiToken = $apiToken;
 
         return $this;
     }
 
+    #[Serializer\VirtualProperty]
+    #[Serializer\SerializedName('apiToken')]
+    #[Serializer\Groups(['Default'])]
+    public function hasApiToken(): bool
+    {
+        return $this->apiToken !== null;
+    }
+
     public function getPlainApiToken(): ?string
     {
         return $this->plainApiToken;
     }
 
@@ -834,13 +842,10 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
     public function hasEmail(): bool
     {
         return $this->email !== null;
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getPassword(): ?string
     {
         return $this->password;
     }
 
@@ -862,13 +867,10 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
     public function getConfirmationToken(): ?string
     {
         return $this->confirmationToken;
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getRoles(): array
     {
         $roles = $this->roles;
 
         // we need to make sure to have at least one role
@@ -961,11 +963,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
         $this->confirmationToken = $confirmationToken;
 
         return $this;
     }
 
-    public function setPasswordRequestedAt(\DateTime $date = null): User
+    public function setPasswordRequestedAt(?\DateTime $date = null): User
     {
         $this->passwordRequestedAt = $date;
 
         return $this;
     }
@@ -1111,10 +1113,24 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
     public function getName(): string
     {
         return $this->getDisplayName();
     }
 
+    public function requiresPasswordReset(): bool
+    {
+        if (!$this->isInternalUser() || !$this->isEnabled()) {
+            return false;
+        }
+
+        return $this->getPreferenceValue('__pw_reset__') === '1';
+    }
+
+    public function setRequiresPasswordReset(bool $require = true): void
+    {
+        $this->setPreferenceValue('__pw_reset__', ($require ? '1' : '0'));
+    }
+
     public function hasSeenWizard(string $wizard): bool
     {
         $wizards = $this->getPreferenceValue('__wizards__');
 
         if (\is_string($wizards)) {
diff --git a/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php b/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php
index df43b97d..75bc2ad9 100644
--- a/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php
+++ b/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php
@@ -43,14 +43,10 @@ final class InvoiceDocumentSubscriber extends AbstractActionsSubscriber
         /** @var string $token */
         $token = \array_key_exists('token', $payload) ? $payload['token'] : null;
 
         $event->addAction('download', ['url' => $this->path('admin_invoice_document_download', ['document' => $document->getId()])]);
 
-        if ($document->isTwig()) {
-            $event->addAction('reload', ['url' => $this->path('admin_invoice_document_reload', ['document' => $document->getId()])]);
-        }
-
         if (!$inUse) {
             $event->addDelete($this->path('invoice_document_delete', ['id' => $document->getId(), 'token' => $token]), false);
         }
     }
 }
diff --git a/src/EventSubscriber/MenuBuilderSubscriber.php b/src/EventSubscriber/MenuBuilderSubscriber.php
index 9876b402..a4f35239 100644
--- a/src/EventSubscriber/MenuBuilderSubscriber.php
+++ b/src/EventSubscriber/MenuBuilderSubscriber.php
@@ -12,12 +12,12 @@ namespace App\EventSubscriber;
 use App\Entity\User;
 use App\Event\ConfigureMainMenuEvent;
 use App\Utils\MenuItemModel;
 use KevinPapst\TablerBundle\Event\MenuEvent;
 use KevinPapst\TablerBundle\Model\MenuItemInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Bundle\SecurityBundle\Security;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
  * Class MenuBuilder configures the main navigation.
  * @internal
diff --git a/src/EventSubscriber/UserPreferenceSubscriber.php b/src/EventSubscriber/UserPreferenceSubscriber.php
index 3dd42d48..16504724 100644
--- a/src/EventSubscriber/UserPreferenceSubscriber.php
+++ b/src/EventSubscriber/UserPreferenceSubscriber.php
@@ -21,11 +21,11 @@ use App\Form\Type\FirstWeekDayType;
 use App\Form\Type\InitialViewType;
 use App\Form\Type\SkinType;
 use App\Form\Type\TimezoneType;
 use App\Form\Type\UserLanguageType;
 use App\Form\Type\YesNoType;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\Form\Extension\Core\Type\MoneyType;
 use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
 use Symfony\Component\Validator\Constraints\Length;
 use Symfony\Component\Validator\Constraints\Range;
diff --git a/src/EventSubscriber/UserProfileSubscriber.php b/src/EventSubscriber/UserProfileSubscriber.php
index 2de79f87..e1a07c16 100644
--- a/src/EventSubscriber/UserProfileSubscriber.php
+++ b/src/EventSubscriber/UserProfileSubscriber.php
@@ -9,11 +9,11 @@
 
 namespace App\EventSubscriber;
 
 use App\Entity\User;
 use App\Event\PrepareUserEvent;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\HttpKernel\Event\KernelEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
 use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
 
diff --git a/src/EventSubscriber/WizardSubscriber.php b/src/EventSubscriber/WizardSubscriber.php
index a2041782..a92b1484 100644
--- a/src/EventSubscriber/WizardSubscriber.php
+++ b/src/EventSubscriber/WizardSubscriber.php
@@ -47,31 +47,34 @@ class WizardSubscriber implements EventSubscriberInterface
         }
 
         $uri = $event->getRequest()->getRequestUri();
 
         // never require 2FA on API calls
-        if (str_starts_with($uri, '/api/') || stripos($uri, '/register/') !== false) {
+        if (str_starts_with($uri, '/api/') || stripos($uri, '/register/') !== false || stripos($uri, '/wizard/') !== false) {
             return;
         }
 
         $user = $token->getUser();
 
-        if ($user instanceof User) {
-            if (stripos($uri, '/wizard/') !== false) {
-                return;
-            }
+        if (!($user instanceof User)) {
+            return;
+        }
 
-            if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
-                return;
-            }
+        if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
+            return;
+        }
 
-            foreach (User::WIZARDS as $wizard) {
-                if (!$user->hasSeenWizard($wizard)) {
-                    $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard]));
-                    $event->setResponse($response);
+        foreach (User::WIZARDS as $wizard) {
+            if (!$user->hasSeenWizard($wizard)) {
+                $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard]));
+                $event->setResponse($response);
 
-                    return;
-                }
+                return;
             }
         }
+
+        if ($user->requiresPasswordReset()) {
+            $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => 'password']));
+            $event->setResponse($response);
+        }
     }
 }
diff --git a/src/Export/Base/AbstractSpreadsheetRenderer.php b/src/Export/Base/AbstractSpreadsheetRenderer.php
index fb8be045..c36dd918 100644
--- a/src/Export/Base/AbstractSpreadsheetRenderer.php
+++ b/src/Export/Base/AbstractSpreadsheetRenderer.php
@@ -28,12 +28,12 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType;
 use PhpOffice\PhpSpreadsheet\Shared\Date;
 use PhpOffice\PhpSpreadsheet\Spreadsheet;
 use PhpOffice\PhpSpreadsheet\Style\Border;
 use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
 use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Bundle\SecurityBundle\Security;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\ResponseHeaderBag;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
diff --git a/src/Export/Base/HtmlRenderer.php b/src/Export/Base/HtmlRenderer.php
index 69bde636..469587c0 100644
--- a/src/Export/Base/HtmlRenderer.php
+++ b/src/Export/Base/HtmlRenderer.php
@@ -19,13 +19,15 @@ use App\Event\ProjectMetaDisplayEvent;
 use App\Event\TimesheetMetaDisplayEvent;
 use App\Event\UserPreferenceDisplayEvent;
 use App\Project\ProjectStatisticService;
 use App\Repository\Query\CustomerQuery;
 use App\Repository\Query\TimesheetQuery;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use App\Twig\SecurityPolicy\ExportPolicy;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Response;
 use Twig\Environment;
+use Twig\Extension\SandboxExtension;
 
 class HtmlRenderer
 {
     use RendererTrait;
 
@@ -91,10 +93,15 @@ class HtmlRenderer
         $this->dispatcher->dispatch($event);
         $userPreferences = $event->getPreferences();
 
         $summary = $this->calculateSummary($timesheets);
 
+        // enable basic security measures
+        $sandbox = new SandboxExtension(new ExportPolicy());
+        $sandbox->enableSandbox();
+        $this->twig->addExtension($sandbox);
+
         $content = $this->twig->render($this->getTemplate(), array_merge([
             'entries' => $timesheets,
             'query' => $query,
             'summaries' => $summary,
             'budgets' => $this->calculateProjectBudget($timesheets, $query, $this->projectStatisticService),
diff --git a/src/Export/Base/PDFRenderer.php b/src/Export/Base/PDFRenderer.php
index be8b3fe5..2f4def2e 100644
--- a/src/Export/Base/PDFRenderer.php
+++ b/src/Export/Base/PDFRenderer.php
@@ -14,12 +14,14 @@ use App\Export\ExportFilename;
 use App\Pdf\HtmlToPdfConverter;
 use App\Pdf\PdfContext;
 use App\Pdf\PdfRendererTrait;
 use App\Project\ProjectStatisticService;
 use App\Repository\Query\TimesheetQuery;
+use App\Twig\SecurityPolicy\ExportPolicy;
 use Symfony\Component\HttpFoundation\Response;
 use Twig\Environment;
+use Twig\Extension\SandboxExtension;
 
 class PDFRenderer implements DispositionInlineInterface
 {
     use RendererTrait;
     use PDFRendererTrait;
@@ -74,10 +76,16 @@ class PDFRenderer implements DispositionInlineInterface
         $filename = new ExportFilename($query);
         $context = new PdfContext();
         $context->setOption('filename', $filename->getFilename());
 
         $summary = $this->calculateSummary($timesheets);
+
+        // enable basic security measures
+        $sandbox = new SandboxExtension(new ExportPolicy());
+        $sandbox->enableSandbox();
+        $this->twig->addExtension($sandbox);
+
         $content = $this->twig->render($this->getTemplate(), array_merge([
             'entries' => $timesheets,
             'query' => $query,
             'summaries' => $summary,
             'budgets' => $this->calculateProjectBudget($timesheets, $query, $this->projectStatisticService),
diff --git a/src/Export/Renderer/HtmlRendererFactory.php b/src/Export/Renderer/HtmlRendererFactory.php
index 3fc269ff..bd7ffa4f 100644
--- a/src/Export/Renderer/HtmlRendererFactory.php
+++ b/src/Export/Renderer/HtmlRendererFactory.php
@@ -9,11 +9,11 @@
 
 namespace App\Export\Renderer;
 
 use App\Activity\ActivityStatisticService;
 use App\Project\ProjectStatisticService;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Twig\Environment;
 
 final class HtmlRendererFactory
 {
     public function __construct(
diff --git a/src/Export/ServiceExport.php b/src/Export/ServiceExport.php
index b7ada6f2..1cb640a8 100644
--- a/src/Export/ServiceExport.php
+++ b/src/Export/ServiceExport.php
@@ -12,11 +12,11 @@ namespace App\Export;
 use App\Entity\ExportableItem;
 use App\Event\ExportItemsQueryEvent;
 use App\Export\Renderer\HtmlRendererFactory;
 use App\Export\Renderer\PdfRendererFactory;
 use App\Repository\Query\ExportQuery;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 final class ServiceExport
 {
     /**
      * @var array<int, string>
diff --git a/src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php b/src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php
index 3f34c545..0644634c 100644
--- a/src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php
+++ b/src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php
@@ -10,11 +10,11 @@
 namespace App\Export\Spreadsheet\Extractor;
 
 use App\Entity\EntityWithMetaFields;
 use App\Event\MetaDisplayEventInterface;
 use App\Export\Spreadsheet\ColumnDefinition;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 /**
  * @internal
  */
 final class MetaFieldExtractor implements ExtractorInterface
diff --git a/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php b/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php
index 4ec5f224..fae27f9c 100644
--- a/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php
+++ b/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php
@@ -10,11 +10,11 @@
 namespace App\Export\Spreadsheet\Extractor;
 
 use App\Entity\User;
 use App\Event\UserPreferenceDisplayEvent;
 use App\Export\Spreadsheet\ColumnDefinition;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 /**
  * @internal
  */
 final class UserPreferenceExtractor implements ExtractorInterface
diff --git a/src/Form/MultiUpdate/TimesheetMultiUpdate.php b/src/Form/MultiUpdate/TimesheetMultiUpdate.php
index f7da39ac..dcf45f20 100644
--- a/src/Form/MultiUpdate/TimesheetMultiUpdate.php
+++ b/src/Form/MultiUpdate/TimesheetMultiUpdate.php
@@ -9,10 +9,11 @@
 
 namespace App\Form\MultiUpdate;
 
 use App\Form\Type\ActivityType;
 use App\Form\Type\CustomerType;
+use App\Form\Type\DescriptionType;
 use App\Form\Type\FixedRateType;
 use App\Form\Type\HourlyRateType;
 use App\Form\Type\MetaFieldsCollectionType;
 use App\Form\Type\ProjectType;
 use App\Form\Type\TagsType;
@@ -139,20 +140,40 @@ final class TimesheetMultiUpdate extends AbstractType
 
         $builder->add('replaceTags', ChoiceType::class, [
             'label' => false,
             'required' => true,
             'expanded' => true,
+            'label_attr' => [
+                'class' => 'radio-inline',
+            ],
             'choices' => [
-                'replaceTags' => true,
-                'appendTags' => false,
+                'append' => false,
+                'replace' => true,
             ]
         ]);
 
         $builder->add('tags', TagsType::class, [
             'required' => false,
         ]);
 
+        $builder->add('replaceDescription', ChoiceType::class, [
+            'label' => false,
+            'required' => true,
+            'expanded' => true,
+            'label_attr' => [
+                'class' => 'radio-inline',
+            ],
+            'choices' => [
+                'append' => false,
+                'replace' => true,
+            ]
+        ]);
+
+        $builder->add('description', DescriptionType::class, [
+            'required' => false,
+        ]);
+
         if ($options['include_user']) {
             $builder->add('user', UserType::class, [
                 'required' => false,
             ]);
         }
diff --git a/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php b/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php
index a893c7b8..9f2af574 100644
--- a/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php
+++ b/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php
@@ -47,10 +47,12 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit
     private Collection $meta;
     /**
      * @var array<string>
      */
     private array $updateMeta = [];
+    private bool $replaceDescription = false;
+    private ?string $description = null;
 
     public function __construct()
     {
         $this->meta = new ArrayCollection();
     }
@@ -96,10 +98,20 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit
     public function setTags(iterable $tags): void
     {
         $this->tags = $tags;
     }
 
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public function setDescription(?string $description): void
+    {
+        $this->description = $description;
+    }
+
     public function getUser(): ?User
     {
         return $this->user;
     }
 
@@ -146,10 +158,20 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit
     public function setReplaceTags(bool $replaceTags): void
     {
         $this->replaceTags = $replaceTags;
     }
 
+    public function isReplaceDescription(): bool
+    {
+        return $this->replaceDescription;
+    }
+
+    public function setReplaceDescription(bool $replaceDescription): void
+    {
+        $this->replaceDescription = $replaceDescription;
+    }
+
     public function getFixedRate(): ?float
     {
         return $this->fixedRate;
     }
 
diff --git a/src/Form/Type/MenuChoiceType.php b/src/Form/Type/MenuChoiceType.php
index 03701f1b..951b4c84 100644
--- a/src/Form/Type/MenuChoiceType.php
+++ b/src/Form/Type/MenuChoiceType.php
@@ -9,11 +9,11 @@
 
 namespace App\Form\Type;
 
 use App\Event\ConfigureMainMenuEvent;
 use App\Utils\MenuItemModel;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 use Symfony\Component\OptionsResolver\Options;
 use Symfony\Component\OptionsResolver\OptionsResolver;
 
diff --git a/src/Form/Type/SkinType.php b/src/Form/Type/SkinType.php
index aef0f899..2216ba45 100644
--- a/src/Form/Type/SkinType.php
+++ b/src/Form/Type/SkinType.php
@@ -24,10 +24,11 @@ final class SkinType extends AbstractType
     ];
 
     public function configureOptions(OptionsResolver $resolver): void
     {
         $resolver->setDefaults([
+            'label' => 'skin',
             'search' => false,
             'required' => true,
             'choices' => self::THEMES,
         ]);
     }
diff --git a/src/Form/UserCreateType.php b/src/Form/UserCreateType.php
index e2780d10..6c5a2dd5 100644
--- a/src/Form/UserCreateType.php
+++ b/src/Form/UserCreateType.php
@@ -9,10 +9,11 @@
 
 namespace App\Form;
 
 use App\Form\Type\TeamType;
 use App\Form\Type\UserRoleType;
+use App\Form\Type\YesNoType;
 use Symfony\Component\Form\Extension\Core\Type\PasswordType;
 use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolver;
 
@@ -61,10 +62,16 @@ class UserCreateType extends UserEditType
                 'multiple' => true,
                 'expanded' => false,
                 'required' => false,
             ]);
         }
+
+        $builder->add('requiresPasswordReset', YesNoType::class, [
+            'label' => 'force_password_change',
+            'help' => 'force_password_change_help',
+            'required' => false,
+        ]);
     }
 
     public function configureOptions(OptionsResolver $resolver): void
     {
         parent::configureOptions($resolver);
diff --git a/src/Invoice/ServiceInvoice.php b/src/Invoice/ServiceInvoice.php
index 01dd51bc..6cbe9d07 100644
--- a/src/Invoice/ServiceInvoice.php
+++ b/src/Invoice/ServiceInvoice.php
@@ -20,13 +20,13 @@ use App\Export\Base\DispositionInlineInterface;
 use App\Model\InvoiceDocument;
 use App\Repository\InvoiceDocumentRepository;
 use App\Repository\InvoiceRepository;
 use App\Repository\Query\InvoiceQuery;
 use App\Utils\FileHelper;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Symfony\Component\HttpFoundation\Response;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 /**
  * Service to manage invoice dependencies.
  */
 final class ServiceInvoice
diff --git a/src/Kernel.php b/src/Kernel.php
index b136ffbd..e769156c 100644
--- a/src/Kernel.php
+++ b/src/Kernel.php
@@ -167,11 +167,11 @@ class Kernel extends BaseKernel
     protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
     {
         $container->registerExtension(new AppExtension());
 
         $container->setParameter('container.autowiring.strict_mode', true);
-        $container->setParameter('container.dumper.inline_class_loader', true);
+        $container->setParameter('.container.dumper.inline_class_loader', true);
         $confDir = $this->getProjectDir() . '/config';
 
         // using this one instead of $loader->load($confDir . '/packages/*' . self::CONFIG_EXTS, 'glob');
         // to get rid of the local.yaml from the list, we load it afterwards explicit
         $finder = (new Finder())
diff --git a/src/Project/ProjectService.php b/src/Project/ProjectService.php
index e1c0c563..ecceaa7f 100644
--- a/src/Project/ProjectService.php
+++ b/src/Project/ProjectService.php
@@ -20,11 +20,11 @@ use App\Event\ProjectUpdatePostEvent;
 use App\Event\ProjectUpdatePreEvent;
 use App\Repository\ProjectRepository;
 use App\Utils\Context;
 use App\Validator\ValidationFailedException;
 use InvalidArgumentException;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Validator\Validator\ValidatorInterface;
 
 /**
  * @final
  */
diff --git a/src/Project/ProjectStatisticService.php b/src/Project/ProjectStatisticService.php
index 4cf6f782..09f06eb0 100644
--- a/src/Project/ProjectStatisticService.php
+++ b/src/Project/ProjectStatisticService.php
@@ -34,11 +34,11 @@ use App\Repository\ProjectRepository;
 use App\Repository\TimesheetRepository;
 use App\Repository\UserRepository;
 use App\Timesheet\DateTimeFactory;
 use DateTime;
 use Doctrine\DBAL\Types\Types;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 /**
  * @final
  */
 class ProjectStatisticService
diff --git a/src/Reporting/ReportingService.php b/src/Reporting/ReportingService.php
index 12ce5ae8..81cb03f8 100644
--- a/src/Reporting/ReportingService.php
+++ b/src/Reporting/ReportingService.php
@@ -9,11 +9,11 @@
 
 namespace App\Reporting;
 
 use App\Entity\User;
 use App\Event\ReportingEvent;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
 
 final class ReportingService
 {
     public function __construct(private EventDispatcherInterface $dispatcher, private AuthorizationCheckerInterface $security)
diff --git a/src/Repository/TimesheetRepository.php b/src/Repository/TimesheetRepository.php
index 6a99ece3..83b2b242 100644
--- a/src/Repository/TimesheetRepository.php
+++ b/src/Repository/TimesheetRepository.php
@@ -353,18 +353,21 @@ class TimesheetRepository extends EntityRepository
 
     public function countActiveEntries(?User $user = null): int
     {
         $qb = $this->getEntityManager()->createQueryBuilder();
 
-        $qb->select('COUNT(t)')
+        $qb->select($qb->expr()->count('t'))
             ->from(Timesheet::class, 't')
             ->andWhere($qb->expr()->isNull('t.end'))
-            ->orderBy('t.begin', 'DESC');
+        ;
 
         if (null !== $user) {
-            $qb->andWhere('t.user = :user');
-            $qb->setParameter('user', $user);
+            $qb
+                ->andWhere('t.user = :user')
+                ->groupBy('t.user')
+                ->setParameter('user', $user)
+            ;
         }
 
         return (int) $qb->getQuery()->getSingleScalarResult();
     }
 
diff --git a/src/Security/TwoFactorCondition.php b/src/Security/TwoFactorCondition.php
index c9f6c5a3..ce501cdc 100644
--- a/src/Security/TwoFactorCondition.php
+++ b/src/Security/TwoFactorCondition.php
@@ -7,11 +7,10 @@
  * file that was distributed with this source code.
  */
 
 namespace App\Security;
 
-use App\Entity\User;
 use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;
 use Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface;
 use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
 
 final class TwoFactorCondition implements TwoFactorConditionInterface
@@ -20,13 +19,10 @@ final class TwoFactorCondition implements TwoFactorConditionInterface
     {
     }
 
     public function shouldPerformTwoFactorAuthentication(AuthenticationContextInterface $context): bool
     {
-        /** @var User $user */
-        $user = $context->getUser();
-
         // never require 2FA on API calls
         if (str_starts_with($context->getRequest()->getRequestUri(), '/api/')) {
             return false;
         }
 
diff --git a/src/Timesheet/TimesheetService.php b/src/Timesheet/TimesheetService.php
index 2f27c764..4856e3f1 100644
--- a/src/Timesheet/TimesheetService.php
+++ b/src/Timesheet/TimesheetService.php
@@ -29,11 +29,11 @@ use App\Repository\TimesheetRepository;
 use App\Security\AccessDeniedException;
 use App\Timesheet\TrackingMode\TrackingModeInterface;
 use App\Validator\ValidationException;
 use App\Validator\ValidationFailedException;
 use InvalidArgumentException;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
 use Symfony\Component\Validator\ConstraintViolation;
 use Symfony\Component\Validator\Validator\ValidatorInterface;
 
diff --git a/src/Twig/Extensions.php b/src/Twig/Extensions.php
index b3f75f6a..25e78b03 100644
--- a/src/Twig/Extensions.php
+++ b/src/Twig/Extensions.php
@@ -19,13 +19,10 @@ use Twig\TwigTest;
 
 final class Extensions extends AbstractExtension
 {
     public const REPORT_DATE = 'Y-m-d';
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFilters(): array
     {
         return [
             new TwigFilter('report_date', [$this, 'formatReportDate']),
             new TwigFilter('docu_link', [$this, 'documentationLink']),
@@ -35,13 +32,10 @@ final class Extensions extends AbstractExtension
             new TwigFilter('default_color', [$this, 'defaultColor']),
             new TwigFilter('nl2str', [$this, 'replaceNewline'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
         ];
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFunctions(): array
     {
         return [
             new TwigFunction('class_name', [$this, 'getClassName']),
             new TwigFunction('iso_day_by_name', [$this, 'getIsoDayByName']),
diff --git a/src/Twig/LocaleFormatExtensions.php b/src/Twig/LocaleFormatExtensions.php
index 1b9736c7..9c044a47 100644
--- a/src/Twig/LocaleFormatExtensions.php
+++ b/src/Twig/LocaleFormatExtensions.php
@@ -31,13 +31,10 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
 
     public function __construct(private LocaleService $localeService, private Security $security)
     {
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFilters(): array
     {
         return [
             new TwigFilter('month_name', [$this, 'monthName']),
             new TwigFilter('day_name', [$this, 'dayName']),
@@ -72,13 +69,10 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
                 return $compare->format('Y-m-d') === $dateTime->format('Y-m-d');
             }),
         ];
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFunctions(): array
     {
         return [
             new TwigFunction('javascript_configurations', [$this, 'getJavascriptConfiguration']),
             new TwigFunction('create_date', [$this, 'createDate']),
diff --git a/src/Twig/PaginationExtension.php b/src/Twig/PaginationExtension.php
index 605d7dd1..ef607d9b 100644
--- a/src/Twig/PaginationExtension.php
+++ b/src/Twig/PaginationExtension.php
@@ -25,13 +25,10 @@ final class PaginationExtension extends AbstractExtension
 
     public function __construct(private UrlGeneratorInterface $router)
     {
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFunctions(): array
     {
         return [
             new TwigFunction('pagination', [$this, 'renderPagination'], ['is_safe' => ['html']]),
         ];
diff --git a/src/Twig/SecurityPolicy/ChainPolicy.php b/src/Twig/SecurityPolicy/ChainPolicy.php
new file mode 100644
index 00000000..5cd30c00
--- /dev/null
+++ b/src/Twig/SecurityPolicy/ChainPolicy.php
@@ -0,0 +1,48 @@
+<?php
+
+/*
+ * This file is part of the Kimai time-tracking app.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Twig\SecurityPolicy;
+
+use Twig\Sandbox\SecurityPolicyInterface;
+
+final class ChainPolicy implements SecurityPolicyInterface
+{
+    /** @var array<SecurityPolicyInterface> */
+    private array $policies = [];
+
+    public function __construct()
+    {
+    }
+
+    public function addPolicy(SecurityPolicyInterface $policy): void
+    {
+        $this->policies[] = $policy;
+    }
+
+    public function checkSecurity($tags, $filters, $functions): void
+    {
+        foreach ($this->policies as $policy) {
+            $policy->checkSecurity($tags, $filters, $functions);
+        }
+    }
+
+    public function checkMethodAllowed($obj, $method): void
+    {
+        foreach ($this->policies as $policy) {
+            $policy->checkMethodAllowed($obj, $method);
+        }
+    }
+
+    public function checkPropertyAllowed($obj, $property): void
+    {
+        foreach ($this->policies as $policy) {
+            $policy->checkPropertyAllowed($obj, $property);
+        }
+    }
+}
diff --git a/src/Twig/SecurityPolicy/DefaultPolicy.php b/src/Twig/SecurityPolicy/DefaultPolicy.php
new file mode 100644
index 00000000..70a88221
--- /dev/null
+++ b/src/Twig/SecurityPolicy/DefaultPolicy.php
@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of the Kimai time-tracking app.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Twig\SecurityPolicy;
+
+use Twig\Sandbox\SecurityPolicyInterface;
+
+/**
+ * The Twig environment needs the sandbox extension, which itself needs a policy to start working.
+ */
+final class DefaultPolicy implements SecurityPolicyInterface
+{
+    public function checkSecurity($tags, $filters, $functions): void
+    {
+    }
+
+    public function checkMethodAllowed($obj, $method): void
+    {
+    }
+
+    public function checkPropertyAllowed($obj, $property): void
+    {
+    }
+}
diff --git a/src/Twig/SecurityPolicy/ExportPolicy.php b/src/Twig/SecurityPolicy/ExportPolicy.php
new file mode 100644
index 00000000..3cacedfb
--- /dev/null
+++ b/src/Twig/SecurityPolicy/ExportPolicy.php
@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * This file is part of the Kimai time-tracking app.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Twig\SecurityPolicy;
+
+use Twig\Sandbox\SecurityPolicyInterface;
+
+/**
+ * Represents the security policy for custom Twig export templates.
+ */
+final class ExportPolicy implements SecurityPolicyInterface
+{
+    private ChainPolicy $policy;
+
+    public function __construct()
+    {
+        $this->policy = new ChainPolicy();
+        $this->policy->addPolicy(new DefaultPolicy());
+    }
+
+    public function checkSecurity($tags, $filters, $functions): void
+    {
+        $this->policy->checkSecurity($tags, $filters, $functions);
+    }
+
+    public function checkMethodAllowed($obj, $method): void
+    {
+        $this->policy->checkMethodAllowed($obj, $method);
+    }
+
+    public function checkPropertyAllowed($obj, $property): void
+    {
+        $this->policy->checkPropertyAllowed($obj, $property);
+    }
+}
diff --git a/src/Twig/SecurityPolicy/ForbiddenPolicy.php b/src/Twig/SecurityPolicy/ForbiddenPolicy.php
new file mode 100644
index 00000000..7c60522b
--- /dev/null
+++ b/src/Twig/SecurityPolicy/ForbiddenPolicy.php
@@ -0,0 +1,109 @@
+<?php
+
+/*
+ * This file is part of the Kimai time-tracking app.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Twig\SecurityPolicy;
+
+use Twig\Markup;
+use Twig\Sandbox\SecurityNotAllowedFilterError;
+use Twig\Sandbox\SecurityNotAllowedFunctionError;
+use Twig\Sandbox\SecurityNotAllowedMethodError;
+use Twig\Sandbox\SecurityNotAllowedPropertyError;
+use Twig\Sandbox\SecurityNotAllowedTagError;
+use Twig\Sandbox\SecurityPolicyInterface;
+use Twig\Template;
+
+/**
+ * A blocking approach for Twig templates.
+ */
+final class ForbiddenPolicy implements SecurityPolicyInterface
+{
+    /** @var array<string, array<string>> */
+    private array $forbiddenMethods = [];
+
+    /**
+     * @param array<string> $forbiddenTags
+     * @param array<string> $forbiddenFilters
+     * @param array<string, array<string>> $forbiddenMethods
+     * @param array<string, array<string>> $forbiddenProperties
+     * @param array<string> $forbiddenFunctions
+     */
+    public function __construct(
+        private array $forbiddenTags = [],
+        private array $forbiddenFilters = [],
+        array $forbiddenMethods = [],
+        private array $forbiddenProperties = [],
+        private array $forbiddenFunctions = []
+    )
+    {
+        $this->forbiddenMethods = [];
+        foreach ($forbiddenMethods as $class => $m) {
+            $this->forbiddenMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]);
+        }
+    }
+
+    public function checkSecurity($tags, $filters, $functions): void
+    {
+        foreach ($tags as $tag) {
+            if (\in_array($tag, $this->forbiddenTags)) {
+                throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag);
+            }
+        }
+
+        foreach ($filters as $filter) {
+            if (\in_array($filter, $this->forbiddenFilters)) {
+                throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter);
+            }
+        }
+
+        foreach ($functions as $function) {
+            if (\in_array($function, $this->forbiddenFunctions)) {
+                throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function);
+            }
+        }
+    }
+
+    public function checkMethodAllowed($obj, $method): void
+    {
+        if ($obj instanceof Template || $obj instanceof Markup) {
+            return;
+        }
+
+        $forbidden = false;
+        $method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
+        foreach ($this->forbiddenMethods as $class => $methods) {
+            if ($obj instanceof $class) {
+                $forbidden = \in_array($method, $methods);
+
+                break;
+            }
+        }
+
+        if ($forbidden) {
+            $class = \get_class($obj);
+            throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
+        }
+    }
+
+    public function checkPropertyAllowed($obj, $property): void
+    {
+        $forbidden = false;
+        foreach ($this->forbiddenProperties as $class => $properties) {
+            if ($obj instanceof $class) {
+                $forbidden = \in_array($property, \is_array($properties) ? $properties : [$properties]);
+
+                break;
+            }
+        }
+
+        if ($forbidden) {
+            $class = \get_class($obj);
+            throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
+        }
+    }
+}
diff --git a/src/Twig/SecurityPolicy/InvoicePolicy.php b/src/Twig/SecurityPolicy/InvoicePolicy.php
new file mode 100644
index 00000000..655b021b
--- /dev/null
+++ b/src/Twig/SecurityPolicy/InvoicePolicy.php
@@ -0,0 +1,86 @@
+<?php
+
+/*
+ * This file is part of the Kimai time-tracking app.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Twig\SecurityPolicy;
+
+use App\Invoice\InvoiceModel;
+use App\Pdf\PdfContext;
+use Symfony\Component\String\UnicodeString;
+use Twig\Markup;
+use Twig\Sandbox\SecurityPolicy;
+use Twig\Sandbox\SecurityPolicyInterface;
+use Twig\Template;
+
+/**
+ * Represents the security policy for custom Twig invoice templates.
+ */
+final class InvoicePolicy implements SecurityPolicyInterface
+{
+    private ChainPolicy $policy;
+
+    public function __construct()
+    {
+        $this->policy = new ChainPolicy();
+        $this->policy->addPolicy(new DefaultPolicy());
+        $this->policy->addPolicy(new SecurityPolicy(
+            ['block', 'if', 'for', 'set', 'extends'],
+            [
+                // Twig core filters
+                'map', 'escape', 'trans', 'default', 'nl2br', 'trim', 'raw',
+                'join', 'u', 'slice', 'date', 'month_name', 'first', 'country_name',
+                'replace', 'length', 'number_format', 'split',
+
+                // Kimai filters
+                'md2html', 'desc2html', 'comment2html', 'comment1line', 'multiline_indent', 'nl2str',
+                'date_short', 'duration', 'amount', 'money', 'duration_decimal',
+            ],
+            [
+                PdfContext::class => ['setoption'],
+                InvoiceModel::class => ['toarray'],
+            ],
+            [], // properties
+            [
+                // Twig core functions
+                'cycle', 'asset', 'range',
+
+                // Kimai functions
+                'encore_entry_css_source', 'qr_code_data_uri', 'config',
+            ]
+        ));
+    }
+
+    public function checkSecurity($tags, $filters, $functions): void
+    {
+        $this->policy->checkSecurity($tags, $filters, $functions);
+    }
+
+    public function checkMethodAllowed($obj, $method): void
+    {
+        if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) {
+            return;
+        }
+
+        $lm = strtolower($method);
+
+        if (str_starts_with($lm, 'get') || str_starts_with($lm, 'is') || str_starts_with($lm, 'has')) {
+            return;
+        }
+
+        if ($lm === '__tostring') {
+            return;
+        }
+
+        $this->policy->checkMethodAllowed($obj, $method);
+    }
+
+    public function checkPropertyAllowed($obj, $property): void
+    {
+        $this->policy->checkPropertyAllowed($obj, $property);
+    }
+}
diff --git a/src/Twig/TwigRendererTrait.php b/src/Twig/TwigRendererTrait.php
index 1d1cadb0..a5ea5bd4 100644
--- a/src/Twig/TwigRendererTrait.php
+++ b/src/Twig/TwigRendererTrait.php
@@ -7,13 +7,15 @@
  * file that was distributed with this source code.
  */
 
 namespace App\Twig;
 
+use App\Twig\SecurityPolicy\InvoicePolicy;
 use Symfony\Bridge\Twig\Extension\TranslationExtension;
 use Symfony\Contracts\Translation\LocaleAwareInterface;
 use Twig\Environment;
+use Twig\Extension\SandboxExtension;
 
 /**
  * @internal
  */
 trait TwigRendererTrait
@@ -28,10 +30,17 @@ trait TwigRendererTrait
         }
         if ($formatLocale !== null) {
             $previousFormatLocale = $this->switchFormatLocale($twig, $formatLocale);
         }
 
+        // enable basic security measures
+        if (!$twig->hasExtension(SandboxExtension::class)) {
+            $sandbox = new SandboxExtension(new InvoicePolicy());
+            $sandbox->enableSandbox();
+            $twig->addExtension($sandbox);
+        }
+
         $content = $twig->render($template, $options);
 
         if ($previousTranslation !== null) {
             $this->switchTranslationLocale($twig, $previousTranslation);
         }
diff --git a/src/User/LoginManager.php b/src/User/LoginManager.php
index ef0e5e0a..128daba5 100644
--- a/src/User/LoginManager.php
+++ b/src/User/LoginManager.php
@@ -10,16 +10,16 @@
 namespace App\User;
 
 use App\Entity\User;
 use App\Event\UserInteractiveLoginEvent;
 use App\Security\UserChecker;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
 use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
 use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 final class LoginManager
 {
     public function __construct(
         private TokenStorageInterface $tokenStorage,
diff --git a/src/User/UserService.php b/src/User/UserService.php
index d7ad8f68..0ad868fd 100644
--- a/src/User/UserService.php
+++ b/src/User/UserService.php
@@ -20,11 +20,11 @@ use App\Event\UserDeletePreEvent;
 use App\Event\UserUpdatePostEvent;
 use App\Event\UserUpdatePreEvent;
 use App\Repository\UserRepository;
 use App\Validator\ValidationFailedException;
 use InvalidArgumentException;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
 use Symfony\Component\Validator\Validator\ValidatorInterface;
 
 /**
  * @final
diff --git a/src/Widget/Type/AbstractActiveUsers.php b/src/Widget/Type/AbstractActiveUsers.php
index b7a12d0f..85173694 100644
--- a/src/Widget/Type/AbstractActiveUsers.php
+++ b/src/Widget/Type/AbstractActiveUsers.php
@@ -10,12 +10,12 @@
 namespace App\Widget\Type;
 
 abstract class AbstractActiveUsers extends AbstractWidgetType
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'icon' => 'users',
diff --git a/src/Widget/Type/AbstractAmountPeriod.php b/src/Widget/Type/AbstractAmountPeriod.php
index 6072ec6d..0f9902dd 100644
--- a/src/Widget/Type/AbstractAmountPeriod.php
+++ b/src/Widget/Type/AbstractAmountPeriod.php
@@ -9,11 +9,11 @@
 
 namespace App\Widget\Type;
 
 use App\Event\RevenueStatisticEvent;
 use App\Repository\TimesheetRepository;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 abstract class AbstractAmountPeriod extends AbstractWidget
 {
     public function __construct(private TimesheetRepository $repository, private EventDispatcherInterface $dispatcher)
     {
@@ -28,22 +28,22 @@ abstract class AbstractAmountPeriod extends AbstractWidget
     {
         return 'widget/widget-counter-money.html.twig';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'icon' => 'money',
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      * @return array<string, float>
      */
     protected function getRevenue(?\DateTimeInterface $begin, ?\DateTimeInterface $end, array $options = []): array
     {
         $data = $this->repository->getRevenue($begin, $end, null);
diff --git a/src/Widget/Type/AbstractCounterDuration.php b/src/Widget/Type/AbstractCounterDuration.php
index 7abcd195..023b228b 100644
--- a/src/Widget/Type/AbstractCounterDuration.php
+++ b/src/Widget/Type/AbstractCounterDuration.php
@@ -10,12 +10,12 @@
 namespace App\Widget\Type;
 
 abstract class AbstractCounterDuration extends AbstractWidgetType
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'icon' => 'duration',
diff --git a/src/Widget/Type/AbstractCounterYear.php b/src/Widget/Type/AbstractCounterYear.php
index c86ba561..efa8c580 100644
--- a/src/Widget/Type/AbstractCounterYear.php
+++ b/src/Widget/Type/AbstractCounterYear.php
@@ -19,11 +19,11 @@ abstract class AbstractCounterYear extends AbstractWidgetType
     public function __construct(private SystemConfiguration $systemConfiguration)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         $begin = $this->createDate('01 january this year 00:00:00');
         $end = $this->createDate('31 december this year 23:59:59');
@@ -37,11 +37,11 @@ abstract class AbstractCounterYear extends AbstractWidgetType
 
         return $this->getYearData($begin, $end, $options);
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     abstract protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed;
 
     abstract protected function getFinancialYearTitle(): string;
 
diff --git a/src/Widget/Type/AbstractUserRevenuePeriod.php b/src/Widget/Type/AbstractUserRevenuePeriod.php
index 4c693eca..a78a86c3 100644
--- a/src/Widget/Type/AbstractUserRevenuePeriod.php
+++ b/src/Widget/Type/AbstractUserRevenuePeriod.php
@@ -9,11 +9,11 @@
 
 namespace App\Widget\Type;
 
 use App\Event\UserRevenueStatisticEvent;
 use App\Repository\TimesheetRepository;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 abstract class AbstractUserRevenuePeriod extends AbstractWidget
 {
     public function __construct(private TimesheetRepository $repository, private EventDispatcherInterface $dispatcher)
     {
@@ -33,22 +33,22 @@ abstract class AbstractUserRevenuePeriod extends AbstractWidget
     {
         return ['view_rate_own_timesheet'];
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'icon' => 'money',
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      * @return array<string, float>
      */
     protected function getRevenue(?\DateTimeInterface $begin, ?\DateTimeInterface $end, array $options = []): array
     {
         $user = $this->getUser();
diff --git a/src/Widget/Type/AbstractWidget.php b/src/Widget/Type/AbstractWidget.php
index 172133bf..bd005f25 100644
--- a/src/Widget/Type/AbstractWidget.php
+++ b/src/Widget/Type/AbstractWidget.php
@@ -66,12 +66,12 @@ abstract class AbstractWidget implements WidgetInterface
     {
         $this->options[$name] = $value;
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     * @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge($this->options, $options);
     }
diff --git a/src/Widget/Type/ActiveTimesheets.php b/src/Widget/Type/ActiveTimesheets.php
index 36cd02ae..35a21a52 100644
--- a/src/Widget/Type/ActiveTimesheets.php
+++ b/src/Widget/Type/ActiveTimesheets.php
@@ -7,10 +7,11 @@
  * file that was distributed with this source code.
  */
 
 namespace App\Widget\Type;
 
+use App\Repository\Query\TimesheetQuery;
 use App\Repository\TimesheetRepository;
 use App\Widget\WidgetException;
 use App\Widget\WidgetInterface;
 
 final class ActiveTimesheets extends AbstractWidgetType
@@ -18,20 +19,29 @@ final class ActiveTimesheets extends AbstractWidgetType
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     * @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
-        return array_merge(['color' => WidgetInterface::COLOR_TOTAL, 'icon' => 'duration'], parent::getOptions($options));
+        // we can safely assume that the user can see
+        $route = 'admin_timesheet';
+
+        return array_merge([
+            'color' => WidgetInterface::COLOR_TOTAL,
+            'icon' => 'duration',
+            'route' => $route,
+            'routeOptions' => ['state' => TimesheetQuery::STATE_RUNNING],
+        ], parent::getOptions($options));
     }
 
     public function getPermissions(): array
     {
+        // if you ever loosen that check, make sure that the above link is probably removed
         return ['view_all_data'];
     }
 
     public function getId(): string
     {
@@ -42,11 +52,11 @@ final class ActiveTimesheets extends AbstractWidgetType
     {
         return 'stats.activeRecordings';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->countActiveEntries();
diff --git a/src/Widget/Type/ActiveUsersMonth.php b/src/Widget/Type/ActiveUsersMonth.php
index 19afc82a..e6fdf6f3 100644
--- a/src/Widget/Type/ActiveUsersMonth.php
+++ b/src/Widget/Type/ActiveUsersMonth.php
@@ -18,12 +18,12 @@ final class ActiveUsersMonth extends AbstractActiveUsers
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
     }
@@ -32,11 +32,11 @@ final class ActiveUsersMonth extends AbstractActiveUsers
     {
         return 'activeUsersMonth';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->countActiveUsers($this->createMonthStartDate(), $this->createMonthEndDate(), null);
diff --git a/src/Widget/Type/ActiveUsersToday.php b/src/Widget/Type/ActiveUsersToday.php
index ed68fffc..98897cfc 100644
--- a/src/Widget/Type/ActiveUsersToday.php
+++ b/src/Widget/Type/ActiveUsersToday.php
@@ -18,12 +18,12 @@ final class ActiveUsersToday extends AbstractActiveUsers
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
     }
@@ -32,11 +32,11 @@ final class ActiveUsersToday extends AbstractActiveUsers
     {
         return 'activeUsersToday';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->countActiveUsers($this->createTodayStartDate(), $this->createTodayEndDate(), null);
diff --git a/src/Widget/Type/ActiveUsersTotal.php b/src/Widget/Type/ActiveUsersTotal.php
index 9dfcb5fc..5a19c817 100644
--- a/src/Widget/Type/ActiveUsersTotal.php
+++ b/src/Widget/Type/ActiveUsersTotal.php
@@ -18,12 +18,12 @@ final class ActiveUsersTotal extends AbstractActiveUsers
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
     }
@@ -32,11 +32,11 @@ final class ActiveUsersTotal extends AbstractActiveUsers
     {
         return 'activeUsersTotal';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->countActiveUsers(null, null, null);
diff --git a/src/Widget/Type/ActiveUsersWeek.php b/src/Widget/Type/ActiveUsersWeek.php
index cc0ca9eb..1a653291 100644
--- a/src/Widget/Type/ActiveUsersWeek.php
+++ b/src/Widget/Type/ActiveUsersWeek.php
@@ -18,12 +18,12 @@ final class ActiveUsersWeek extends AbstractActiveUsers
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
     }
@@ -32,11 +32,11 @@ final class ActiveUsersWeek extends AbstractActiveUsers
     {
         return 'activeUsersWeek';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->countActiveUsers($this->createWeekStartDate(), $this->createWeekEndDate(), null);
diff --git a/src/Widget/Type/ActiveUsersYear.php b/src/Widget/Type/ActiveUsersYear.php
index 76ed8b6a..13f59f5e 100644
--- a/src/Widget/Type/ActiveUsersYear.php
+++ b/src/Widget/Type/ActiveUsersYear.php
@@ -20,23 +20,23 @@ final class ActiveUsersYear extends AbstractCounterYear
     {
         parent::__construct($systemConfiguration);
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'icon' => 'users',
             'color' => WidgetInterface::COLOR_YEAR,
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
     {
         try {
             return $this->repository->countActiveUsers($begin, $end, null);
diff --git a/src/Widget/Type/AmountMonth.php b/src/Widget/Type/AmountMonth.php
index c8d2d123..c160c28b 100644
--- a/src/Widget/Type/AmountMonth.php
+++ b/src/Widget/Type/AmountMonth.php
@@ -12,12 +12,12 @@ namespace App\Widget\Type;
 use App\Widget\WidgetInterface;
 
 final class AmountMonth extends AbstractAmountPeriod
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
     }
@@ -26,11 +26,11 @@ final class AmountMonth extends AbstractAmountPeriod
     {
         return 'AmountMonth';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         return $this->getRevenue($this->createMonthStartDate(), $this->createMonthEndDate(), $options);
     }
diff --git a/src/Widget/Type/AmountToday.php b/src/Widget/Type/AmountToday.php
index fdb0947c..7b09a8db 100644
--- a/src/Widget/Type/AmountToday.php
+++ b/src/Widget/Type/AmountToday.php
@@ -12,12 +12,12 @@ namespace App\Widget\Type;
 use App\Widget\WidgetInterface;
 
 final class AmountToday extends AbstractAmountPeriod
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
     }
@@ -26,11 +26,11 @@ final class AmountToday extends AbstractAmountPeriod
     {
         return 'AmountToday';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         return $this->getRevenue($this->createTodayStartDate(), $this->createTodayEndDate(), $options);
     }
diff --git a/src/Widget/Type/AmountTotal.php b/src/Widget/Type/AmountTotal.php
index d3bc6ec8..b3f3fd7e 100644
--- a/src/Widget/Type/AmountTotal.php
+++ b/src/Widget/Type/AmountTotal.php
@@ -12,12 +12,12 @@ namespace App\Widget\Type;
 use App\Widget\WidgetInterface;
 
 final class AmountTotal extends AbstractAmountPeriod
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
     }
@@ -31,11 +31,11 @@ final class AmountTotal extends AbstractAmountPeriod
     {
         return ['view_all_data'];
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         return $this->getRevenue(null, null, $options);
     }
diff --git a/src/Widget/Type/AmountWeek.php b/src/Widget/Type/AmountWeek.php
index 38ecded3..6509b93a 100644
--- a/src/Widget/Type/AmountWeek.php
+++ b/src/Widget/Type/AmountWeek.php
@@ -12,12 +12,12 @@ namespace App\Widget\Type;
 use App\Widget\WidgetInterface;
 
 final class AmountWeek extends AbstractAmountPeriod
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
     }
@@ -26,11 +26,11 @@ final class AmountWeek extends AbstractAmountPeriod
     {
         return 'AmountWeek';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         return $this->getRevenue($this->createWeekStartDate(), $this->createWeekEndDate(), $options);
     }
diff --git a/src/Widget/Type/AmountYear.php b/src/Widget/Type/AmountYear.php
index e18ebfb9..f5c164d7 100644
--- a/src/Widget/Type/AmountYear.php
+++ b/src/Widget/Type/AmountYear.php
@@ -13,33 +13,33 @@ use App\Configuration\SystemConfiguration;
 use App\Event\RevenueStatisticEvent;
 use App\Model\Revenue;
 use App\Repository\TimesheetRepository;
 use App\Widget\WidgetException;
 use App\Widget\WidgetInterface;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 final class AmountYear extends AbstractCounterYear
 {
     public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher)
     {
         parent::__construct($systemConfiguration);
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'icon' => 'money',
             'color' => WidgetInterface::COLOR_YEAR,
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
     {
         try {
             /** @var array<Revenue> $data */
diff --git a/src/Widget/Type/DailyWorkingTimeChart.php b/src/Widget/Type/DailyWorkingTimeChart.php
index 651a63db..7f15c50b 100644
--- a/src/Widget/Type/DailyWorkingTimeChart.php
+++ b/src/Widget/Type/DailyWorkingTimeChart.php
@@ -44,12 +44,12 @@ final class DailyWorkingTimeChart extends AbstractWidget
     {
         return true;
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'begin' => null,
@@ -59,11 +59,11 @@ final class DailyWorkingTimeChart extends AbstractWidget
             'id' => uniqid('DailyWorkingTimeChart_'),
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         $user = $this->getUser();
         $dateTimeFactory = DateTimeFactory::createByUser($user);
diff --git a/src/Widget/Type/DurationMonth.php b/src/Widget/Type/DurationMonth.php
index d9645f60..26a33214 100644
--- a/src/Widget/Type/DurationMonth.php
+++ b/src/Widget/Type/DurationMonth.php
@@ -18,12 +18,12 @@ final class DurationMonth extends AbstractCounterDuration
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
     }
@@ -42,11 +42,11 @@ final class DurationMonth extends AbstractCounterDuration
     {
         return 'stats.durationMonth';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange($this->createMonthStartDate(), $this->createMonthEndDate(), null);
diff --git a/src/Widget/Type/DurationToday.php b/src/Widget/Type/DurationToday.php
index 4ed11138..3923909b 100644
--- a/src/Widget/Type/DurationToday.php
+++ b/src/Widget/Type/DurationToday.php
@@ -18,12 +18,12 @@ final class DurationToday extends AbstractCounterDuration
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
     }
@@ -42,11 +42,11 @@ final class DurationToday extends AbstractCounterDuration
     {
         return 'stats.durationToday';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange($this->createTodayStartDate(), $this->createTodayEndDate(), null);
diff --git a/src/Widget/Type/DurationTotal.php b/src/Widget/Type/DurationTotal.php
index 298f959b..b818d1a5 100644
--- a/src/Widget/Type/DurationTotal.php
+++ b/src/Widget/Type/DurationTotal.php
@@ -18,12 +18,12 @@ final class DurationTotal extends AbstractCounterDuration
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
     }
@@ -42,11 +42,11 @@ final class DurationTotal extends AbstractCounterDuration
     {
         return 'stats.durationTotal';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange(null, null, null);
diff --git a/src/Widget/Type/DurationWeek.php b/src/Widget/Type/DurationWeek.php
index 1a98dba0..e7cd0d5a 100644
--- a/src/Widget/Type/DurationWeek.php
+++ b/src/Widget/Type/DurationWeek.php
@@ -18,12 +18,12 @@ final class DurationWeek extends AbstractCounterDuration
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
     }
@@ -42,11 +42,11 @@ final class DurationWeek extends AbstractCounterDuration
     {
         return 'stats.durationWeek';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange($this->createWeekStartDate(), $this->createWeekEndDate(), null);
diff --git a/src/Widget/Type/DurationYear.php b/src/Widget/Type/DurationYear.php
index 2e366f9b..7d0b8652 100644
--- a/src/Widget/Type/DurationYear.php
+++ b/src/Widget/Type/DurationYear.php
@@ -20,23 +20,23 @@ final class DurationYear extends AbstractCounterYear
     {
         parent::__construct($systemConfiguration);
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'icon' => 'duration',
             'color' => WidgetInterface::COLOR_YEAR,
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange($begin, $end, null);
diff --git a/src/Widget/Type/PaginatedWorkingTimeChart.php b/src/Widget/Type/PaginatedWorkingTimeChart.php
index 9a65e45d..116921b1 100644
--- a/src/Widget/Type/PaginatedWorkingTimeChart.php
+++ b/src/Widget/Type/PaginatedWorkingTimeChart.php
@@ -46,12 +46,12 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
     {
         return 'widget/widget-paginatedworkingtimechart.html.twig';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         $options = parent::getOptions($options);
 
@@ -85,11 +85,11 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
 
         return $lastWeekInYear->format('W') === '53' ? 53 : 52;
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         $user = $this->getUser();
 
diff --git a/src/Widget/Type/TotalsActivity.php b/src/Widget/Type/TotalsActivity.php
index c6adfa26..ab2fbe53 100644
--- a/src/Widget/Type/TotalsActivity.php
+++ b/src/Widget/Type/TotalsActivity.php
@@ -23,12 +23,12 @@ final class TotalsActivity extends AbstractWidget
     {
         return 'stats.activityTotal';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'route' => 'admin_activity',
@@ -36,11 +36,11 @@ final class TotalsActivity extends AbstractWidget
             'color' => WidgetInterface::COLOR_TOTAL,
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         $user = $this->getUser();
         $query = new ActivityQuery();
diff --git a/src/Widget/Type/TotalsCustomer.php b/src/Widget/Type/TotalsCustomer.php
index 78e3db72..a883b957 100644
--- a/src/Widget/Type/TotalsCustomer.php
+++ b/src/Widget/Type/TotalsCustomer.php
@@ -23,12 +23,12 @@ final class TotalsCustomer extends AbstractWidget
     {
         return 'stats.customerTotal';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'route' => 'admin_customer',
@@ -36,11 +36,11 @@ final class TotalsCustomer extends AbstractWidget
             'color' => WidgetInterface::COLOR_TOTAL,
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         $user = $this->getUser();
         $query = new CustomerQuery();
diff --git a/src/Widget/Type/TotalsProject.php b/src/Widget/Type/TotalsProject.php
index 74f81e3a..156c843f 100644
--- a/src/Widget/Type/TotalsProject.php
+++ b/src/Widget/Type/TotalsProject.php
@@ -23,12 +23,12 @@ final class TotalsProject extends AbstractWidget
     {
         return 'stats.projectTotal';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'route' => 'admin_project',
@@ -36,11 +36,11 @@ final class TotalsProject extends AbstractWidget
             'color' => WidgetInterface::COLOR_TOTAL,
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         $user = $this->getUser();
         $query = new ProjectQuery();
diff --git a/src/Widget/Type/TotalsUser.php b/src/Widget/Type/TotalsUser.php
index e78af8ef..9cd11c6e 100644
--- a/src/Widget/Type/TotalsUser.php
+++ b/src/Widget/Type/TotalsUser.php
@@ -18,12 +18,12 @@ final class TotalsUser extends AbstractWidget
     public function __construct(private UserRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'route' => 'admin_user',
@@ -31,11 +31,11 @@ final class TotalsUser extends AbstractWidget
             'color' => WidgetInterface::COLOR_TOTAL,
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         $user = $this->getUser();
         $query = new UserQuery();
diff --git a/src/Widget/Type/UserAmountMonth.php b/src/Widget/Type/UserAmountMonth.php
index d7fc862b..e8bb6765 100644
--- a/src/Widget/Type/UserAmountMonth.php
+++ b/src/Widget/Type/UserAmountMonth.php
@@ -12,12 +12,12 @@ namespace App\Widget\Type;
 use App\Widget\WidgetInterface;
 
 final class UserAmountMonth extends AbstractUserRevenuePeriod
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
     }
@@ -26,11 +26,11 @@ final class UserAmountMonth extends AbstractUserRevenuePeriod
     {
         return 'UserAmountMonth';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         return $this->getRevenue($this->createMonthStartDate(), $this->createMonthEndDate(), $options);
     }
diff --git a/src/Widget/Type/UserAmountToday.php b/src/Widget/Type/UserAmountToday.php
index 914b460c..afb1a2da 100644
--- a/src/Widget/Type/UserAmountToday.php
+++ b/src/Widget/Type/UserAmountToday.php
@@ -12,12 +12,12 @@ namespace App\Widget\Type;
 use App\Widget\WidgetInterface;
 
 final class UserAmountToday extends AbstractUserRevenuePeriod
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
     }
@@ -26,11 +26,11 @@ final class UserAmountToday extends AbstractUserRevenuePeriod
     {
         return 'UserAmountToday';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         return $this->getRevenue($this->createTodayStartDate(), $this->createTodayEndDate(), $options);
     }
diff --git a/src/Widget/Type/UserAmountTotal.php b/src/Widget/Type/UserAmountTotal.php
index 96601c36..ab3fbe80 100644
--- a/src/Widget/Type/UserAmountTotal.php
+++ b/src/Widget/Type/UserAmountTotal.php
@@ -12,12 +12,12 @@ namespace App\Widget\Type;
 use App\Widget\WidgetInterface;
 
 final class UserAmountTotal extends AbstractUserRevenuePeriod
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
     }
@@ -26,11 +26,11 @@ final class UserAmountTotal extends AbstractUserRevenuePeriod
     {
         return 'UserAmountTotal';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         return $this->getRevenue(null, null, $options);
     }
diff --git a/src/Widget/Type/UserAmountWeek.php b/src/Widget/Type/UserAmountWeek.php
index a9ab5991..6d7ff2b0 100644
--- a/src/Widget/Type/UserAmountWeek.php
+++ b/src/Widget/Type/UserAmountWeek.php
@@ -12,12 +12,12 @@ namespace App\Widget\Type;
 use App\Widget\WidgetInterface;
 
 final class UserAmountWeek extends AbstractUserRevenuePeriod
 {
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
     }
@@ -26,11 +26,11 @@ final class UserAmountWeek extends AbstractUserRevenuePeriod
     {
         return 'UserAmountWeek';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         return $this->getRevenue($this->createWeekStartDate(), $this->createWeekEndDate(), $options);
     }
diff --git a/src/Widget/Type/UserAmountYear.php b/src/Widget/Type/UserAmountYear.php
index 00a1566b..0e59a3be 100644
--- a/src/Widget/Type/UserAmountYear.php
+++ b/src/Widget/Type/UserAmountYear.php
@@ -13,11 +13,11 @@ use App\Configuration\SystemConfiguration;
 use App\Event\UserRevenueStatisticEvent;
 use App\Model\Revenue;
 use App\Repository\TimesheetRepository;
 use App\Widget\WidgetException;
 use App\Widget\WidgetInterface;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 final class UserAmountYear extends AbstractCounterYear
 {
     public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher)
     {
@@ -43,23 +43,23 @@ final class UserAmountYear extends AbstractCounterYear
     {
         return 'UserAmountYear';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'icon' => 'money',
             'color' => WidgetInterface::COLOR_YEAR,
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
     {
         try {
             /** @var array<Revenue> $data */
diff --git a/src/Widget/Type/UserDurationMonth.php b/src/Widget/Type/UserDurationMonth.php
index 89d7fae4..3b67243a 100644
--- a/src/Widget/Type/UserDurationMonth.php
+++ b/src/Widget/Type/UserDurationMonth.php
@@ -18,12 +18,12 @@ final class UserDurationMonth extends AbstractCounterDuration
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
     }
@@ -37,11 +37,11 @@ final class UserDurationMonth extends AbstractCounterDuration
     {
         return 'userDurationMonth';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange($this->createMonthStartDate(), $this->createMonthEndDate(), $this->getUser());
diff --git a/src/Widget/Type/UserDurationToday.php b/src/Widget/Type/UserDurationToday.php
index 2e6fcbf8..49cdb2c6 100644
--- a/src/Widget/Type/UserDurationToday.php
+++ b/src/Widget/Type/UserDurationToday.php
@@ -18,12 +18,12 @@ final class UserDurationToday extends AbstractCounterDuration
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
     }
@@ -37,11 +37,11 @@ final class UserDurationToday extends AbstractCounterDuration
     {
         return 'userDurationToday';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange($this->createTodayStartDate(), $this->createTodayEndDate(), $this->getUser());
diff --git a/src/Widget/Type/UserDurationTotal.php b/src/Widget/Type/UserDurationTotal.php
index 9fe86e89..61a4c00d 100644
--- a/src/Widget/Type/UserDurationTotal.php
+++ b/src/Widget/Type/UserDurationTotal.php
@@ -18,12 +18,12 @@ final class UserDurationTotal extends AbstractCounterDuration
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
     }
@@ -37,11 +37,11 @@ final class UserDurationTotal extends AbstractCounterDuration
     {
         return 'userDurationTotal';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange(null, null, $this->getUser());
diff --git a/src/Widget/Type/UserDurationWeek.php b/src/Widget/Type/UserDurationWeek.php
index 0b086836..e3170fc2 100644
--- a/src/Widget/Type/UserDurationWeek.php
+++ b/src/Widget/Type/UserDurationWeek.php
@@ -18,12 +18,12 @@ final class UserDurationWeek extends AbstractCounterDuration
     public function __construct(private TimesheetRepository $repository)
     {
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
     }
@@ -37,11 +37,11 @@ final class UserDurationWeek extends AbstractCounterDuration
     {
         return 'userDurationWeek';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange($this->createWeekStartDate(), $this->createWeekEndDate(), $this->getUser());
diff --git a/src/Widget/Type/UserDurationYear.php b/src/Widget/Type/UserDurationYear.php
index 207cb8a4..3305153e 100644
--- a/src/Widget/Type/UserDurationYear.php
+++ b/src/Widget/Type/UserDurationYear.php
@@ -30,23 +30,23 @@ final class UserDurationYear extends AbstractCounterYear
     {
         return 'widget/widget-counter-duration.html.twig';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array
     {
         return array_merge([
             'icon' => 'duration',
             'color' => WidgetInterface::COLOR_YEAR,
         ], parent::getOptions($options));
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
     {
         try {
             return $this->repository->getDurationForTimeRange($begin, $end, $this->getUser());
diff --git a/src/Widget/Type/UserTeamProjects.php b/src/Widget/Type/UserTeamProjects.php
index 0409db7e..37b4177a 100644
--- a/src/Widget/Type/UserTeamProjects.php
+++ b/src/Widget/Type/UserTeamProjects.php
@@ -58,11 +58,11 @@ final class UserTeamProjects extends AbstractWidget
     {
         return 'UserTeamProjects';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         $user = $this->getUser();
         $now = new \DateTime('now', new \DateTimeZone($user->getTimezone()));
diff --git a/src/Widget/Type/UserTeams.php b/src/Widget/Type/UserTeams.php
index 5a135c1d..8fa30503 100644
--- a/src/Widget/Type/UserTeams.php
+++ b/src/Widget/Type/UserTeams.php
@@ -51,11 +51,11 @@ final class UserTeams extends AbstractWidget
     {
         return 'UserTeams';
     }
 
     /**
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      */
     public function getData(array $options = []): mixed
     {
         $user = $this->getUser();
 
diff --git a/src/Widget/WidgetInterface.php b/src/Widget/WidgetInterface.php
index ee1672fd..61e8fc74 100644
--- a/src/Widget/WidgetInterface.php
+++ b/src/Widget/WidgetInterface.php
@@ -69,11 +69,11 @@ interface WidgetInterface
      *
      * If your widget relies on options to dynamically change the result data,
      * make sure that the given $options will overwrite the internal option for
      * this one call.
      *
-     * @param array<string, string|bool|int|null> $options
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
      * @return mixed|null
      */
     public function getData(array $options = []): mixed;
 
     /**
@@ -83,12 +83,12 @@ interface WidgetInterface
      * overwrite the default values one time.
      *
      * You can validate the options or simply return:
      * return array_merge($this->options, $options);
      *
-     * @param array<string, string|bool|int|null> $options
-     * @return array<string, string|bool|int|null>
+     * @param array<string, string|bool|int|null|array<string, mixed>> $options
+     * @return array<string, string|bool|int|null|array<string, mixed>>
      */
     public function getOptions(array $options = []): array;
 
     /**
      * Sets one widget option, both for internal use and for frontend rendering.
diff --git a/src/WorkingTime/WorkingTimeService.php b/src/WorkingTime/WorkingTimeService.php
index 49c71fcd..f3a7cbf4 100644
--- a/src/WorkingTime/WorkingTimeService.php
+++ b/src/WorkingTime/WorkingTimeService.php
@@ -18,11 +18,11 @@ use App\Repository\TimesheetRepository;
 use App\Repository\WorkingTimeRepository;
 use App\Timesheet\DateTimeFactory;
 use App\WorkingTime\Model\Month;
 use App\WorkingTime\Model\Year;
 use App\WorkingTime\Model\YearPerUserSummary;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
 
 /**
  * @internal this API and the entire namespace is instable: you should expect changes!
  */
 final class WorkingTimeService
diff --git a/symfony.lock b/symfony.lock
index 16c3ce89..6aebb4f6 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -509,13 +509,10 @@
             "branch": "master",
             "version": "3.3",
             "ref": "179470cb6492db92dffee208cfdb436f175c93b4"
         }
     },
-    "symfony/polyfill-ctype": {
-        "version": "v1.8.0"
-    },
     "symfony/polyfill-intl-grapheme": {
         "version": "v1.13.1"
     },
     "symfony/polyfill-intl-icu": {
         "version": "v1.6.0"
diff --git a/templates/reporting/report_by_user.html.twig b/templates/reporting/report_by_user.html.twig
index 9231ca08..e4edfc72 100644
--- a/templates/reporting/report_by_user.html.twig
+++ b/templates/reporting/report_by_user.html.twig
@@ -5,10 +5,11 @@
         {% block period_name %}
             <th class="text-center text-nowrap{% if column is weekend(user) %} weekend{% endif %}{% if column is today %} today{% endif %}">
                 {{ column|date_weekday }}
             </th>
         {% endblock %}
+        {% block upper_left %}&nbsp;{% endblock %}
         {% block column_classes_project -%}
             {% if column.date is weekend(user) %} weekend{% endif %}{% if column.date is today %} today{% endif %}
         {%- endblock %}
         {% block column_classes_activity -%}
             {% if column.date is weekend(user) %} weekend{% endif %}{% if column.date is today %} today{% endif %}
diff --git a/templates/reporting/report_by_user_data.html.twig b/templates/reporting/report_by_user_data.html.twig
index 0572ef73..1da8516c 100644
--- a/templates/reporting/report_by_user_data.html.twig
+++ b/templates/reporting/report_by_user_data.html.twig
@@ -15,14 +15,16 @@
 {% set columns = 0 %}
 {% set totalCurrency = false %}
 <table class="table table-hover dataTable">
     <thead>
         <tr>
-            <th>&nbsp;</th>
+            <th>{% block upper_left %}{{ user.displayName }}{% endblock %}</th>
             <th class="text-center reportDataTypeTitle">{{ dataTypeTitle|trans }}</th>
             {% for column in period.dateTimes %}
-                {% block period_name %}{% endblock %}
+                {% block period_name %}
+                    <th>{{ column|date_short }}</th>
+                {% endblock %}
                 {% set columns = columns + 1 %}
                 {% set dateKey = column|report_date %}
                 {% set totalsDuration = totalsDuration|merge({(dateKey): 0}) %}
                 {% set totalsInternalRate = totalsInternalRate|merge({(dateKey): 0}) %}
                 {% set totalsRate = totalsRate|merge({(dateKey): 0}) %}
diff --git a/templates/reporting/report_by_user_layout.html.twig b/templates/reporting/report_by_user_layout.html.twig
index c8e63b2d..49e4ce5c 100644
--- a/templates/reporting/report_by_user_layout.html.twig
+++ b/templates/reporting/report_by_user_layout.html.twig
@@ -7,10 +7,12 @@
         {{ form_widget(form.user, {'label': false}) }}
     {% elseif app.user != user %}
         {{ widgets.username(user) }}
     {% endif %}
     {{ form_widget(form.sumType) }}
+    {% from '@theme/components/buttons.html.twig' import submit_button %}
+    {{ submit_button('download', {'attr': {'formaction': path(export_route)}, 'icon': 'download', 'combined': false}, 'primary') }}
 {% endblock %}
 
 {% block report %}
     {% embed '@theme/embeds/card.html.twig' with {report_content: block('report_content'), box_id: box_id, fullsize: true} only %}
         {% import "macros/widgets.html.twig" as widgets %}
diff --git a/templates/reporting/report_by_user_year.html.twig b/templates/reporting/report_by_user_year.html.twig
index fb7c6728..35544a71 100644
--- a/templates/reporting/report_by_user_year.html.twig
+++ b/templates/reporting/report_by_user_year.html.twig
@@ -8,10 +8,11 @@
                     {{ column|month_name }}<br>
                     {{ column|date_format('Y') }}
                 </a>
             </th>
         {% endblock %}
+        {% block upper_left %}&nbsp;{% endblock %}
         {% block column_classes_project %}{% endblock %}
         {% block column_classes_activity %}{% endblock %}
         {% block column_classes_total %}{% endblock %}
     {% endembed %}
 {% endblock %}
diff --git a/templates/reporting/report_by_user_year_export.html.twig b/templates/reporting/report_by_user_year_export.html.twig
new file mode 100644
index 00000000..8347bd7f
--- /dev/null
+++ b/templates/reporting/report_by_user_year_export.html.twig
@@ -0,0 +1,8 @@
+{% embed 'reporting/report_by_user_data.html.twig' %}
+    {% block period_name %}
+        <th>{{ column|month_name }} {{ column|date_format('Y') }}</th>
+    {% endblock %}
+    {% block column_classes_project %}{% endblock %}
+    {% block column_classes_activity %}{% endblock %}
+    {% block column_classes_total %}{% endblock %}
+{% endembed %}
diff --git a/templates/security/password-reset/check_email.html.twig b/templates/security/password-reset/check_email.html.twig
index 80bc47c6..e04313b7 100644
--- a/templates/security/password-reset/check_email.html.twig
+++ b/templates/security/password-reset/check_email.html.twig
@@ -1,11 +1,11 @@
 {% extends 'security/password-reset/layout.html.twig' %}
 
 {% block login_form %}
 
     <p>
-        {{ 'resetting.check_email'|trans({'%tokenLifetime%': tokenLifetime})|nl2br }}
+        {{ 'resetting.check_email'|trans({'%tokenLifetime%': tokenLifetime|duration})|nl2br }}
     </p>
 
 {% endblock %}
 
 
diff --git a/templates/timesheet/multi-update.html.twig b/templates/timesheet/multi-update.html.twig
index 28a062f1..3122dc04 100644
--- a/templates/timesheet/multi-update.html.twig
+++ b/templates/timesheet/multi-update.html.twig
@@ -20,11 +20,15 @@
                     <div class="col-md-4">
                         {{ form_row(form.activity) }}
                     </div>
                 </div>
             </fieldset>
-            <fieldset class="form-fieldset pb-0">
+            <fieldset class="form-fieldset pb-0 h-100">
+                {{ form_row(form.replaceDescription) }}
+                {{ form_row(form.description) }}
+            </fieldset>
+            <fieldset class="form-fieldset pb-0 h-100">
                 {{ form_row(form.replaceTags) }}
                 {{ form_row(form.tags) }}
             </fieldset>
             {% if form.user is defined %}
                 <fieldset class="form-fieldset pb-0">
diff --git a/templates/user/api-token.html.twig b/templates/user/api-token.html.twig
index 9a4762b4..cbb72eab 100644
--- a/templates/user/api-token.html.twig
+++ b/templates/user/api-token.html.twig
@@ -11,11 +11,15 @@
         </div>
     {% endif %}
 
     <div class="row mb-3">
         <div class="d-flex">
-            {{ 'api_password.intro'|trans }}
+            <div class="me-auto">
+                <p>{{ 'api_password.intro'|trans }}</p>
+
+                <p>{{ 'username'|trans }}: {{ user.userIdentifier }}</p>
+            </div>
 
             <div class="ms-auto">
                 <a class="btn btn-icon" target="_blank" href="{{ path('api.swagger_ui') }}" data-toggle="tooltip" title="Swagger API Docs">{{ icon('documentation', true) }}</a>
                 <a class="btn btn-icon" target="_blank" href="{{ 'rest-api.html'|docu_link }}" data-toggle="tooltip" title="{{ 'help'|trans({}, 'about') }}">{{ icon('help', true) }}</a>
             </div>
diff --git a/templates/widget/widget-counter.html.twig b/templates/widget/widget-counter.html.twig
index 769bd3a3..2f43771d 100644
--- a/templates/widget/widget-counter.html.twig
+++ b/templates/widget/widget-counter.html.twig
@@ -5,24 +5,25 @@
 
 <div class="card card-sm">
     <div class="card-body">
         <div class="row align-items-center">
             <div class="col-auto">
+                {% if not url is empty %}<a href="{{ url }}">{% endif %}
                 <span class="bg-{{ options.color|default('green') }} text-white avatar">
                     <i class="{{ options.icon|icon(true, options.icon) }}"></i>
                 </span>
+                {% if not url is empty %}</a>{% endif %}
             </div>
             <div class="col">
-                {% if not url is empty %}<a href="{{ url }}">{% endif %}
+
                 <div class="font-weight-medium">
                     {{ title|trans }}
                 </div>
                 <div class="text-body-secondary">
                     {% block widget_data %}
                         {{ data }}
                     {% endblock %}
                 </div>
-                {% if not url is empty %}</a>{% endif %}
             </div>
         </div>
     </div>
 </div>
diff --git a/templates/wizard/layout.html.twig b/templates/wizard/layout.html.twig
index 14a9c0e1..2e9552f8 100644
--- a/templates/wizard/layout.html.twig
+++ b/templates/wizard/layout.html.twig
@@ -18,23 +18,25 @@
         {{ form_end(form) }}
     {% endif %}
 {% endblock %}
 
 {% block wizard_progress_bar %}
-    {{ progress_bar({current: percent, max: 100, min: 0}) }}
+    {% if percent is defined and percent is not null %}
+        {{ progress_bar({current: percent, max: 100, min: 0}) }}
+    {% endif %}
 {% endblock %}
 
 {% block wizard_previous_button %}
-    {% if previous is defined %}
+    {% if previous is defined and previous is not null %}
         {{ button(false, { title : 'Previous'|trans({}, 'TablerBundle'), combined: true, url: path('wizard', {'wizard': previous}) }, 'link') }}
     {% endif %}
 {% endblock %}
 
 {% block wizard_next_button %}
     {% if form is defined %}
         {{ submit_button(false, { title : 'Next'|trans({}, 'TablerBundle'), combined: true }, 'primary') }}
     {% else %}
-        {% if next is defined %}
+        {% if next is defined and next is not null %}
             {{ button(false, { title : 'Next'|trans({}, 'TablerBundle'), combined: true, url: path('wizard', {'wizard': next}) }, 'primary') }}
         {% endif %}
     {% endif %}
 {% endblock %}
diff --git a/templates/wizard/password.html.twig b/templates/wizard/password.html.twig
new file mode 100644
index 00000000..9d6ae750
--- /dev/null
+++ b/templates/wizard/password.html.twig
@@ -0,0 +1,14 @@
+{% extends 'wizard/layout.html.twig' %}
+
+{% block wizard_content %}
+    <div class="card-body text-center py-4 p-sm-6">
+        <h1 class="mt-2">{{ 'wizard.password.title'|trans({}, 'wizard') }}</h1>
+        <p class="text-body-secondary">{{ 'wizard.password.description'|trans({}, 'wizard') }}</p>
+    </div>
+    <div class="hr-text hr-text-center hr-text-spaceless">{{ 'wizard.separator'|trans({}, 'wizard') }}</div>
+    <div class="card-body">
+        <div class="mb-3">
+            {{ form_rest(form) }}
+        </div>
+    </div>
+{% endblock %}
diff --git a/tests/API/APIControllerBaseTest.php b/tests/API/APIControllerBaseTest.php
index 35a0e876..f225f535 100644
--- a/tests/API/APIControllerBaseTest.php
+++ b/tests/API/APIControllerBaseTest.php
@@ -338,10 +338,11 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
             case 'UserCollection':
                 return [
                     'id' => 'int',
                     'username' => 'string',
                     'enabled' => 'bool',
+                    'apiToken' => 'bool',
                     'color' => '@string',
                     'alias' => '@string',
                     'accountNumber' => '@string',
                     'initials' => '@string',
                     'title' => '@string',
@@ -351,10 +352,11 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
             case 'UserEntity':
                 return [
                     'id' => 'int',
                     'username' => 'string',
                     'enabled' => 'bool',
+                    'apiToken' => 'bool',
                     'alias' => '@string',
                     'title' => '@string',
                     'supervisor' => ['result' => 'object', 'type' => '@UserEntity'],
                     'avatar' => '@string',
                     'color' => '@string',
diff --git a/tests/API/UserControllerTest.php b/tests/API/UserControllerTest.php
index cd16edd3..a679d1de 100644
--- a/tests/API/UserControllerTest.php
+++ b/tests/API/UserControllerTest.php
@@ -43,25 +43,46 @@ class UserControllerTest extends APIControllerBaseTest
 
     public function testGetCollection(): void
     {
         $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
         $this->assertAccessIsGranted($client, '/api/users');
-        $result = json_decode($client->getResponse()->getContent(), true);
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
 
         $this->assertIsArray($result);
         $this->assertNotEmpty($result);
         $this->assertEquals(7, \count($result));
         foreach ($result as $user) {
             self::assertApiResponseTypeStructure('UserCollection', $user);
         }
     }
 
+    public function testGetCollectionFull(): void
+    {
+        $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
+        $this->assertAccessIsGranted($client, '/api/users?full=true');
+
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
+
+        $this->assertIsArray($result);
+        $this->assertNotEmpty($result);
+        $this->assertEquals(7, \count($result));
+        foreach ($result as $user) {
+            self::assertApiResponseTypeStructure('UserEntity', $user);
+        }
+    }
+
     public function testGetCollectionWithQuery(): void
     {
         $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
         $this->assertAccessIsGranted($client, '/api/users', 'GET', ['visible' => 2, 'orderBy' => 'email', 'order' => 'DESC', 'term' => 'chris']);
-        $result = json_decode($client->getResponse()->getContent(), true);
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
 
         $this->assertIsArray($result);
         $this->assertNotEmpty($result);
         $this->assertEquals(1, \count($result));
         foreach ($result as $user) {
@@ -71,11 +92,13 @@ class UserControllerTest extends APIControllerBaseTest
 
     public function testGetCollectionWithQuery2(): void
     {
         $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
         $this->assertAccessIsGranted($client, '/api/users', 'GET', ['visible' => 3, 'orderBy' => 'email', 'order' => 'DESC']);
-        $result = json_decode($client->getResponse()->getContent(), true);
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
 
         $this->assertIsArray($result);
         $this->assertNotEmpty($result);
         $this->assertEquals(8, \count($result));
         foreach ($result as $user) {
@@ -85,11 +108,13 @@ class UserControllerTest extends APIControllerBaseTest
 
     public function testGetEntity(): void
     {
         $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
         $this->assertAccessIsGranted($client, '/api/users/1');
-        $result = json_decode($client->getResponse()->getContent(), true);
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
 
         $this->assertIsArray($result);
         self::assertApiResponseTypeStructure('UserEntity', $result);
         self::assertEquals('1', $result['id']);
         self::assertEquals('CFO', $result['title']);
@@ -98,11 +123,13 @@ class UserControllerTest extends APIControllerBaseTest
 
     public function testGetMyProfile(): void
     {
         $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
         $this->assertAccessIsGranted($client, '/api/users/me');
-        $result = json_decode($client->getResponse()->getContent(), true);
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
 
         $this->assertIsArray($result);
         self::assertApiResponseTypeStructure('UserEntity', $result);
         self::assertEquals('6', $result['id']);
         self::assertEquals('Super Administrator', $result['title']);
@@ -122,11 +149,13 @@ class UserControllerTest extends APIControllerBaseTest
 
     public function testGetEntityAccessAllowedForOwnProfile(): void
     {
         $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
         $this->assertAccessIsGranted($client, '/api/users/2');
-        $result = json_decode($client->getResponse()->getContent(), true);
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
 
         $this->assertIsArray($result);
         self::assertApiResponseTypeStructure('UserEntity', $result);
     }
 
@@ -148,11 +177,13 @@ class UserControllerTest extends APIControllerBaseTest
             ],
         ];
         $this->request($client, '/api/users', 'POST', [], json_encode($data));
         $this->assertTrue($client->getResponse()->isSuccessful());
 
-        $result = json_decode($client->getResponse()->getContent(), true);
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
         $this->assertIsArray($result);
         self::assertApiResponseTypeStructure('UserEntity', $result);
         $this->assertNotEmpty($result['id']);
         self::assertEquals('foo', $result['username']);
         self::assertEquals('asdfghjkl', $result['title']);
@@ -236,11 +267,13 @@ class UserControllerTest extends APIControllerBaseTest
                 'ROLE_ADMIN'
             ],
         ];
         $this->request($client, '/api/users', 'POST', [], json_encode($data));
         $this->assertTrue($client->getResponse()->isSuccessful());
-        $result = json_decode($client->getResponse()->getContent(), true);
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
         self::assertFalse($result['enabled']);
 
         $data = [
             'title' => 'qwertzui',
             'enabled' => true,
@@ -251,11 +284,13 @@ class UserControllerTest extends APIControllerBaseTest
             ],
         ];
         $this->request($client, '/api/users/' . $result['id'], 'PATCH', [], json_encode($data));
         $this->assertTrue($client->getResponse()->isSuccessful());
 
-        $result = json_decode($client->getResponse()->getContent(), true);
+        $content = $client->getResponse()->getContent();
+        $this->assertIsString($content);
+        $result = json_decode($content, true);
         $this->assertIsArray($result);
         self::assertApiResponseTypeStructure('UserEntity', $result);
         $this->assertNotEmpty($result['id']);
         self::assertEquals('foo', $result['username']);
         self::assertEquals('qwertzui', $result['title']);
diff --git a/tests/Command/UserLoginLinkCommandTest.php b/tests/Command/UserLoginLinkCommandTest.php
new file mode 100644
index 00000000..541b0014
--- /dev/null
+++ b/tests/Command/UserLoginLinkCommandTest.php
@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of the Kimai time-tracking app.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Tests\Command;
+
+use App\Command\UserLoginLinkCommand;
+use App\Repository\UserRepository;
+use Symfony\Bundle\FrameworkBundle\Console\Application;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
+
+/**
+ * @covers \App\Command\UserLoginLinkCommand
+ * @group integration
+ */
+class UserLoginLinkCommandTest extends KernelTestCase
+{
+    private Application $application;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $kernel = self::bootKernel();
+        $this->application = new Application($kernel);
+
+        $loginLinkHandler = $this->createMock(LoginLinkHandlerInterface::class);
+        $userRepository = $this->createMock(UserRepository::class);
+        $requestStack = $this->createMock(RequestStack::class);
+
+        $this->application->add(new UserLoginLinkCommand($loginLinkHandler, $userRepository, $requestStack));
+    }
+
+    public function testCommandName(): void
+    {
+        $application = $this->application;
+
+        $command = $application->find('kimai:user:login-link');
+        self::assertInstanceOf(UserLoginLinkCommand::class, $command);
+    }
+}
diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php
index 86193052..9d0614d1 100644
--- a/tests/DependencyInjection/ConfigurationTest.php
+++ b/tests/DependencyInjection/ConfigurationTest.php
@@ -307,11 +307,11 @@ class ConfigurationTest extends TestCase
                 'defaults' => [
                     0 => 'var/invoices/',
                     1 => 'templates/invoice/renderer/',
                 ],
                 'number_format' => '{Y}/{cy,3}',
-                'upload_twig' => true,
+                'upload_twig' => false,
             ],
             'export' => [
                 'documents' => [
                 ],
                 'defaults' => [
diff --git a/tests/Entity/TimesheetTest.php b/tests/Entity/TimesheetTest.php
index 918173e6..5d91738c 100644
--- a/tests/Entity/TimesheetTest.php
+++ b/tests/Entity/TimesheetTest.php
@@ -32,11 +32,11 @@ class TimesheetTest extends TestCase
         self::assertEquals('work', $sut->getCategory());
         self::assertNull($sut->getId());
         self::assertNull($sut->getBegin());
         self::assertNull($sut->getEnd());
         self::assertTrue($sut->isBillable());
-        self::assertNull($sut->getModifiedAt());
+        self::assertNotNull($sut->getModifiedAt());
         self::assertSame(0, $sut->getDuration());
         self::assertSame(0, $sut->getDuration(true));
         self::assertSame(0, $sut->getDuration(false));
         self::assertNull($sut->getCalculatedDuration());
         self::assertNull($sut->getUser());
@@ -177,11 +177,11 @@ class TimesheetTest extends TestCase
     }
 
     public function testClone(): void
     {
         $sut = new Timesheet();
-        self::assertNull($sut->getModifiedAt());
+        self::assertNotNull($sut->getModifiedAt());
         $sut->setExported(true);
         $sut->setDescription('Invalid timesheet category "foo" given, expected one of: work, holiday, sickness, parental, overtime');
 
         $modifiedDate = new \DateTime();
 
@@ -208,11 +208,11 @@ class TimesheetTest extends TestCase
         $sut->addTag($tag);
 
         $clone = clone $sut;
 
         self::assertNotNull($sut->getModifiedAt());
-        self::assertNull($clone->getModifiedAt());
+        self::assertNotNull($clone->getModifiedAt());
 
         foreach ($sut->getMetaFields() as $metaField) {
             $cloneMeta = $clone->getMetaField($metaField->getName());
             self::assertEquals($cloneMeta->getValue(), $metaField->getValue());
         }
diff --git a/tests/Export/Renderer/PdfRendererTest.php b/tests/Export/Renderer/PdfRendererTest.php
index 1491bc4f..d08acb40 100644
--- a/tests/Export/Renderer/PdfRendererTest.php
+++ b/tests/Export/Renderer/PdfRendererTest.php
@@ -13,21 +13,22 @@ use App\Export\Renderer\PDFRenderer;
 use App\Pdf\HtmlToPdfConverter;
 use App\Pdf\MPdfConverter;
 use App\Project\ProjectStatisticService;
 use App\Tests\Mocks\FileHelperFactory;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Twig\Environment;
 
 /**
  * @covers \App\Export\Base\PDFRenderer
  * @covers \App\Export\Base\RendererTrait
  * @covers \App\Export\Renderer\PDFRenderer
  * @group integration
  */
 class PdfRendererTest extends AbstractRendererTest
 {
-    public function testConfiguration()
+    public function testConfiguration(): void
     {
         $sut = new PDFRenderer(
             $this->createMock(Environment::class),
             $this->createMock(HtmlToPdfConverter::class),
             $this->createMock(ProjectStatisticService::class)
@@ -41,16 +42,18 @@ class PdfRendererTest extends AbstractRendererTest
         $sut->setPdfOption('foo', 'bar');
         $sut->setPdfOption('bar1', 'foo1');
         $this->assertEquals(['foo' => 'bar', 'bar1' => 'foo1'], $sut->getPdfOptions());
     }
 
-    public function testRender()
+    public function testRenderAttachment(): void
     {
         $kernel = self::bootKernel();
         /** @var Environment $twig */
         $twig = self::getContainer()->get('twig');
+        /** @var RequestStack $stack */
         $stack = self::getContainer()->get('request_stack');
+        /** @var string $cacheDir */
         $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
         $converter = new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir);
         $request = new Request();
         $request->setLocale('en');
         $stack->push($request);
@@ -61,11 +64,32 @@ class PdfRendererTest extends AbstractRendererTest
 
         $response = $this->render($sut);
         $this->assertEquals('application/pdf', $response->headers->get('Content-Type'));
         $this->assertEquals('attachment; filename=' . $prefix . '-Customer_Name-project_name.pdf', $response->headers->get('Content-Disposition'));
         $this->assertNotEmpty($response->getContent());
+    }
+
+    public function testRenderInline(): void
+    {
+        $kernel = self::bootKernel();
+        /** @var Environment $twig */
+        $twig = self::getContainer()->get('twig');
+        /** @var RequestStack $stack */
+        $stack = self::getContainer()->get('request_stack');
+        /** @var string $cacheDir */
+        $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
+        $converter = new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir);
+        $request = new Request();
+        $request->setLocale('en');
+        $stack->push($request);
+
+        $sut = new PDFRenderer($twig, $converter, $this->createMock(ProjectStatisticService::class));
+
+        $prefix = date('Ymd');
 
         $sut->setDispositionInline(true);
         $response = $this->render($sut);
+        $this->assertEquals('application/pdf', $response->headers->get('Content-Type'));
         $this->assertEquals('inline; filename=' . $prefix . '-Customer_Name-project_name.pdf', $response->headers->get('Content-Disposition'));
+        $this->assertNotEmpty($response->getContent());
     }
 }
diff --git a/tests/Export/Timesheet/HtmlRendererTest.php b/tests/Export/Timesheet/HtmlRendererTest.php
index 36ec4b70..5bf0d4b7 100644
--- a/tests/Export/Timesheet/HtmlRendererTest.php
+++ b/tests/Export/Timesheet/HtmlRendererTest.php
@@ -12,19 +12,20 @@ namespace App\Tests\Export\Timesheet;
 use App\Activity\ActivityStatisticService;
 use App\Export\Timesheet\HtmlRenderer;
 use App\Project\ProjectStatisticService;
 use Symfony\Component\EventDispatcher\EventDispatcher;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Twig\Environment;
 
 /**
  * @covers \App\Export\Timesheet\HtmlRenderer
  * @group integration
  */
 class HtmlRendererTest extends AbstractRendererTest
 {
-    public function testConfiguration()
+    public function testConfiguration(): void
     {
         $sut = new HtmlRenderer(
             $this->createMock(Environment::class),
             new EventDispatcher(),
             $this->createMock(ProjectStatisticService::class),
@@ -32,15 +33,16 @@ class HtmlRendererTest extends AbstractRendererTest
         );
 
         $this->assertEquals('print', $sut->getId());
     }
 
-    public function testRender()
+    public function testRender(): void
     {
         $kernel = self::bootKernel();
         /** @var Environment $twig */
         $twig = self::getContainer()->get('twig');
+        /** @var RequestStack $stack */
         $stack = self::getContainer()->get('request_stack');
         $request = new Request();
         $request->setLocale('en');
         $stack->push($request);
 
diff --git a/tests/Export/Timesheet/PdfRendererTest.php b/tests/Export/Timesheet/PdfRendererTest.php
index b866d4f1..e8ab60a8 100644
--- a/tests/Export/Timesheet/PdfRendererTest.php
+++ b/tests/Export/Timesheet/PdfRendererTest.php
@@ -13,37 +13,40 @@ use App\Export\Timesheet\PDFRenderer;
 use App\Pdf\HtmlToPdfConverter;
 use App\Pdf\MPdfConverter;
 use App\Project\ProjectStatisticService;
 use App\Tests\Mocks\FileHelperFactory;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Twig\Environment;
 
 /**
  * @covers \App\Export\Base\PDFRenderer
  * @covers \App\Export\Base\RendererTrait
  * @covers \App\Export\Timesheet\PDFRenderer
  * @group integration
  */
 class PdfRendererTest extends AbstractRendererTest
 {
-    public function testConfiguration()
+    public function testConfiguration(): void
     {
         $sut = new PDFRenderer(
             $this->createMock(Environment::class),
             $this->createMock(HtmlToPdfConverter::class),
             $this->createMock(ProjectStatisticService::class)
         );
 
         $this->assertEquals('pdf', $sut->getId());
     }
 
-    public function testRender()
+    public function testRender(): void
     {
         $kernel = self::bootKernel();
         /** @var Environment $twig */
         $twig = self::getContainer()->get('twig');
+        /** @var RequestStack $stack */
         $stack = self::getContainer()->get('request_stack');
+        /** @var string $cacheDir */
         $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
         $converter = new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir);
         $request = new Request();
         $request->setLocale('en');
         $stack->push($request);
diff --git a/tests/Invoice/Renderer/PdfRendererTest.php b/tests/Invoice/Renderer/PdfRendererTest.php
index c5282668..5fd0d8e0 100644
--- a/tests/Invoice/Renderer/PdfRendererTest.php
+++ b/tests/Invoice/Renderer/PdfRendererTest.php
@@ -8,15 +8,17 @@
  */
 
 namespace App\Tests\Invoice\Renderer;
 
 use App\Invoice\Renderer\PdfRenderer;
+use App\Model\InvoiceDocument;
 use App\Pdf\HtmlToPdfConverter;
 use App\Pdf\MPdfConverter;
 use App\Tests\Mocks\FileHelperFactory;
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Twig\Environment;
 use Twig\Loader\ArrayLoader;
 use Twig\Loader\FilesystemLoader;
 
 /**
@@ -26,11 +28,11 @@ use Twig\Loader\FilesystemLoader;
  */
 class PdfRendererTest extends KernelTestCase
 {
     use RendererTestTrait;
 
-    public function testSupports()
+    public function testSupports(): void
     {
         $env = new Environment(new ArrayLoader([]));
         $sut = new PdfRenderer($env, $this->createMock(HtmlToPdfConverter::class));
         $this->assertTrue($sut->supports($this->getInvoiceDocument('default.pdf.twig', true)));
         $this->assertTrue($sut->supports($this->getInvoiceDocument('service-date.pdf.twig')));
@@ -38,15 +40,16 @@ class PdfRendererTest extends KernelTestCase
         $this->assertFalse($sut->supports($this->getInvoiceDocument('company.docx', true)));
         $this->assertFalse($sut->supports($this->getInvoiceDocument('spreadsheet.xlsx', true)));
         $this->assertFalse($sut->supports($this->getInvoiceDocument('open-spreadsheet.ods', true)));
     }
 
-    public function testRender()
+    public function testRenderAttachment(): void
     {
         $kernel = self::bootKernel();
         /** @var Environment $twig */
         $twig = self::getContainer()->get('twig');
+        /** @var RequestStack $stack */
         $stack = self::getContainer()->get('request_stack');
         $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
 
         $request = new Request();
         $request->setLocale('en');
@@ -62,11 +65,85 @@ class PdfRendererTest extends KernelTestCase
 
         $response = $sut->render($document, $model);
         $this->assertEquals('application/pdf', $response->headers->get('Content-Type'));
         $this->assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition'));
         $this->assertNotEmpty($response->getContent());
+    }
+
+    public function testRenderInline(): void
+    {
+        $kernel = self::bootKernel();
+        /** @var Environment $twig */
+        $twig = self::getContainer()->get('twig');
+        /** @var RequestStack $stack */
+        $stack = self::getContainer()->get('request_stack');
+        /** @var string $cacheDir */
+        $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
+
+        $request = new Request();
+        $request->setLocale('en');
+        $stack->push($request);
+
+        /** @var FilesystemLoader $loader */
+        $loader = $twig->getLoader();
+        $loader->addPath(__DIR__ . '/../templates/', 'invoice');
+
+        $sut = new PdfRenderer($twig, new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir));
+        $model = $this->getInvoiceModel();
+        $document = $this->getInvoiceDocument('default.pdf.twig', true);
 
         $sut->setDispositionInline(true);
         $response = $sut->render($document, $model);
         $this->assertStringContainsString('inline; filename', $response->headers->get('Content-Disposition'));
+        $this->assertNotEmpty($response->getContent());
+    }
+
+    public function testRenderAll(): void
+    {
+        $kernel = self::bootKernel();
+        /** @var Environment $twig */
+        $twig = self::getContainer()->get('twig');
+        $stack = self::getContainer()->get('request_stack');
+        /** @var string $cacheDir */
+        $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
+
+        $request = new Request();
+        $request->setLocale('en');
+        $stack->push($request);
+
+        /** @var FilesystemLoader $loader */
+        $loader = $twig->getLoader();
+        $loader->addPath(__DIR__ . '/../templates/', 'invoice');
+
+        $dirs = [
+            __DIR__ . '/../../../templates/invoice/renderer/',
+            __DIR__ . '/../../../var/invoices/',
+            __DIR__ . '/../../../var/invoices_customer/',
+            __DIR__ . '/../../../var/invoices_old/',
+        ];
+
+        $files = [];
+        foreach ($dirs as $dir) {
+            if (!is_dir($dir)) {
+                continue;
+            }
+            $dir = realpath($dir);
+            $loader->addPath($dir . '/', 'invoice');
+            $found = glob($dir . '/*.pdf.twig');
+            if ($found !== false) {
+                $files = array_merge($files, $found);
+            }
+        }
+
+        $sut = new PdfRenderer($twig, new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir));
+        $model = $this->getInvoiceModel();
+
+        foreach ($files as $filename) {
+            $document = new InvoiceDocument(new \SplFileInfo($filename));
+
+            $response = $sut->render($document, $model);
+            $this->assertEquals('application/pdf', $response->headers->get('Content-Type'));
+            $this->assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition'));
+            $this->assertNotEmpty($response->getContent());
+        }
     }
 }
diff --git a/tests/Invoice/Renderer/TwigRendererTest.php b/tests/Invoice/Renderer/TwigRendererTest.php
index bd076648..a24ad3ac 100644
--- a/tests/Invoice/Renderer/TwigRendererTest.php
+++ b/tests/Invoice/Renderer/TwigRendererTest.php
@@ -8,12 +8,14 @@
  */
 
 namespace App\Tests\Invoice\Renderer;
 
 use App\Invoice\Renderer\TwigRenderer;
+use App\Model\InvoiceDocument;
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Twig\Environment;
 use Twig\Loader\FilesystemLoader;
 
 /**
  * @covers \App\Invoice\Renderer\AbstractTwigRenderer
@@ -22,11 +24,11 @@ use Twig\Loader\FilesystemLoader;
  */
 class TwigRendererTest extends KernelTestCase
 {
     use RendererTestTrait;
 
-    public function testSupports()
+    public function testSupports(): void
     {
         $loader = new FilesystemLoader();
         $env = new Environment($loader);
         $sut = new TwigRenderer($env);
 
@@ -36,15 +38,16 @@ class TwigRendererTest extends KernelTestCase
         $this->assertFalse($sut->supports($this->getInvoiceDocument('company.docx', true)));
         $this->assertFalse($sut->supports($this->getInvoiceDocument('spreadsheet.xlsx', true)));
         $this->assertFalse($sut->supports($this->getInvoiceDocument('open-spreadsheet.ods', true)));
     }
 
-    public function testRender()
+    public function testRender(): void
     {
         $kernel = self::bootKernel();
         /** @var Environment $twig */
         $twig = self::getContainer()->get('twig');
+        /** @var RequestStack $stack */
         $stack = self::getContainer()->get('request_stack');
         $request = new Request();
         $request->setLocale('en');
         $stack->push($request);
 
@@ -53,11 +56,11 @@ class TwigRendererTest extends KernelTestCase
         $loader->addPath($this->getInvoiceTemplatePath(), 'invoice');
 
         $sut = new TwigRenderer($twig);
 
         $model = $this->getInvoiceModel();
-        $model->getTemplate()->setLanguage('de');
+        $model->getTemplate()?->setLanguage('de');
 
         $document = $this->getInvoiceDocument('timesheet.html.twig');
         $response = $sut->render($document, $model);
 
         $content = $response->getContent();
@@ -73,6 +76,55 @@ class TwigRendererTest extends KernelTestCase
     'foo' . PHP_EOL .
     "bar\n" .
     "bar\r\n" .
     'Hello'), $content);
     }
+
+    public function testRenderAll(): void
+    {
+        $kernel = self::bootKernel();
+        /** @var Environment $twig */
+        $twig = self::getContainer()->get('twig');
+        /** @var RequestStack $stack */
+        $stack = self::getContainer()->get('request_stack');
+        $request = new Request();
+        $request->setLocale('en');
+        $stack->push($request);
+
+        /** @var FilesystemLoader $loader */
+        $loader = $twig->getLoader();
+        $loader->addPath($this->getInvoiceTemplatePath(), 'invoice');
+
+        $dirs = [
+            __DIR__ . '/../../../templates/invoice/renderer/',
+            __DIR__ . '/../../../var/invoices/',
+            __DIR__ . '/../../../var/invoices_customer/',
+            __DIR__ . '/../../../var/invoices_old/',
+        ];
+
+        $files = [];
+        foreach ($dirs as $dir) {
+            if (!is_dir($dir)) {
+                continue;
+            }
+            $dir = realpath($dir);
+            $loader->addPath($dir . '/', 'invoice');
+            $found = glob($dir . '/*.html.twig');
+            if ($found !== false) {
+                $files = array_merge($files, $found);
+            }
+        }
+
+        $sut = new TwigRenderer($twig);
+
+        $model = $this->getInvoiceModel();
+        $model->getTemplate()?->setLanguage('de');
+
+        foreach ($files as $filename) {
+            $document = new InvoiceDocument(new \SplFileInfo($filename));
+
+            $response = $sut->render($document, $model);
+            $this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-Type'));
+            $this->assertNotEmpty($response->getContent());
+        }
+    }
 }
diff --git a/tests/Invoice/templates/default.pdf.twig b/tests/Invoice/templates/default.pdf.twig
index 99fffdfe..2d575240 100644
--- a/tests/Invoice/templates/default.pdf.twig
+++ b/tests/Invoice/templates/default.pdf.twig
@@ -1,8 +1,7 @@
 <!DOCTYPE html>
-{% set fallback = app.request is not null ? app.request.locale : 'en' %}
-{% set language = model.template.language|default(fallback) %}
+{% set language = invoice['invoice.language'] %}
 {% set currency = model.currency %}
 <html lang="{{ language }}">
 <head>
     <meta charset="utf-8">
     <style type="text/css">
diff --git a/tests/phpstan.neon b/tests/phpstan.neon
index 24e0b8c5..e97254e5 100644
--- a/tests/phpstan.neon
+++ b/tests/phpstan.neon
@@ -1235,15 +1235,10 @@ parameters:
         -
             message: "#^Cannot access offset 'id' on mixed\\.$#"
             count: 1
             path: API/UserControllerTest.php
 
-        -
-            message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
-            count: 9
-            path: API/UserControllerTest.php
-
         -
             message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
             count: 8
             path: API/UserControllerTest.php
 
@@ -1671,11 +1666,11 @@ parameters:
             message: "#^Parameter \\#5 \\$userRepository of class App\\\\Command\\\\InvoiceCreateCommand constructor expects App\\\\Repository\\\\UserRepository, object\\|null given\\.$#"
             count: 1
             path: Command/InvoiceCreateCommandTest.php
 
         -
-            message: "#^Parameter \\#6 \\$eventDispatcher of class App\\\\Command\\\\InvoiceCreateCommand constructor expects Symfony\\\\Contracts\\\\EventDispatcher\\\\EventDispatcherInterface, object\\|null given\\.$#"
+            message: "#^Parameter \\#6 \\$eventDispatcher of class App\\\\Command\\\\InvoiceCreateCommand constructor expects Psr\\\\EventDispatcher\\\\EventDispatcherInterface, object\\|null given\\.$#"
             count: 1
             path: Command/InvoiceCreateCommandTest.php
 
         -
             message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:getCommandTester\\(\\) has no return type specified\\.$#"
@@ -5175,30 +5170,10 @@ parameters:
         -
             message: "#^Method App\\\\Tests\\\\Export\\\\Renderer\\\\PdfRendererFactoryTest\\:\\:testCreate\\(\\) has no return type specified\\.$#"
             count: 1
             path: Export/Renderer/PdfRendererFactoryTest.php
 
-        -
-            message: "#^Cannot call method push\\(\\) on object\\|null\\.$#"
-            count: 1
-            path: Export/Renderer/PdfRendererTest.php
-
-        -
-            message: "#^Method App\\\\Tests\\\\Export\\\\Renderer\\\\PdfRendererTest\\:\\:testConfiguration\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Export/Renderer/PdfRendererTest.php
-
-        -
-            message: "#^Method App\\\\Tests\\\\Export\\\\Renderer\\\\PdfRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Export/Renderer/PdfRendererTest.php
-
-        -
-            message: "#^Parameter \\#2 \\$cacheDirectory of class App\\\\Pdf\\\\MPdfConverter constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#"
-            count: 1
-            path: Export/Renderer/PdfRendererTest.php
-
         -
             message: "#^Call to an undefined method App\\\\Export\\\\ExportRendererInterface\\|App\\\\Export\\\\TimesheetExportInterface\\:\\:getIcon\\(\\)\\.$#"
             count: 1
             path: Export/Renderer/XlsxRendererTest.php
 
@@ -5630,50 +5605,15 @@ parameters:
         -
             message: "#^Parameter \\#2 \\$haystack of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, string\\|false given\\.$#"
             count: 1
             path: Export/Timesheet/CsvRendererTest.php
 
-        -
-            message: "#^Cannot call method push\\(\\) on object\\|null\\.$#"
-            count: 1
-            path: Export/Timesheet/HtmlRendererTest.php
-
-        -
-            message: "#^Method App\\\\Tests\\\\Export\\\\Timesheet\\\\HtmlRendererTest\\:\\:testConfiguration\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Export/Timesheet/HtmlRendererTest.php
-
-        -
-            message: "#^Method App\\\\Tests\\\\Export\\\\Timesheet\\\\HtmlRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Export/Timesheet/HtmlRendererTest.php
-
         -
             message: "#^Parameter \\#2 \\$haystack of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, string\\|false given\\.$#"
             count: 1
             path: Export/Timesheet/HtmlRendererTest.php
 
-        -
-            message: "#^Cannot call method push\\(\\) on object\\|null\\.$#"
-            count: 1
-            path: Export/Timesheet/PdfRendererTest.php
-
-        -
-            message: "#^Method App\\\\Tests\\\\Export\\\\Timesheet\\\\PdfRendererTest\\:\\:testConfiguration\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Export/Timesheet/PdfRendererTest.php
-
-        -
-            message: "#^Method App\\\\Tests\\\\Export\\\\Timesheet\\\\PdfRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Export/Timesheet/PdfRendererTest.php
-
-        -
-            message: "#^Parameter \\#2 \\$cacheDirectory of class App\\\\Pdf\\\\MPdfConverter constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#"
-            count: 1
-            path: Export/Timesheet/PdfRendererTest.php
-
         -
             message: "#^Method App\\\\Tests\\\\Export\\\\Timesheet\\\\XlsxRendererTest\\:\\:testConfiguration\\(\\) has no return type specified\\.$#"
             count: 1
             path: Export/Timesheet/XlsxRendererTest.php
 
@@ -6455,55 +6395,25 @@ parameters:
         -
             message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\PdfRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
             count: 1
             path: Invoice/Renderer/PdfRendererTest.php
 
-        -
-            message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\PdfRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Invoice/Renderer/PdfRendererTest.php
-
-        -
-            message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\PdfRendererTest\\:\\:testSupports\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Invoice/Renderer/PdfRendererTest.php
-
         -
             message: "#^Parameter \\#2 \\$cacheDirectory of class App\\\\Pdf\\\\MPdfConverter constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#"
             count: 1
             path: Invoice/Renderer/PdfRendererTest.php
 
         -
             message: "#^Parameter \\#2 \\$haystack of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, string\\|null given\\.$#"
-            count: 2
+            count: 3
             path: Invoice/Renderer/PdfRendererTest.php
 
-        -
-            message: "#^Cannot call method push\\(\\) on object\\|null\\.$#"
-            count: 1
-            path: Invoice/Renderer/TwigRendererTest.php
-
-        -
-            message: "#^Cannot call method setLanguage\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#"
-            count: 1
-            path: Invoice/Renderer/TwigRendererTest.php
-
         -
             message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\TwigRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
             count: 1
             path: Invoice/Renderer/TwigRendererTest.php
 
-        -
-            message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\TwigRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Invoice/Renderer/TwigRendererTest.php
-
-        -
-            message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\TwigRendererTest\\:\\:testSupports\\(\\) has no return type specified\\.$#"
-            count: 1
-            path: Invoice/Renderer/TwigRendererTest.php
-
         -
             message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|false given\\.$#"
             count: 1
             path: Invoice/Renderer/TwigRendererTest.php
 
diff --git a/translations/messages.ar.xlf b/translations/messages.ar.xlf
index 6a55be80..0895dcec 100644
--- a/translations/messages.ar.xlf
+++ b/translations/messages.ar.xlf
@@ -754,14 +754,10 @@
       </trans-unit>
       <trans-unit id="8W_TZI0" resname="searchTerm">
         <source>searchTerm</source>
         <target state="translated">مصطلح البحث</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>استبدل العلامات</target>
-      </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>اللون</target>
       </trans-unit>
       <trans-unit id="b43sCwO" resname="help.fixedRate">
@@ -802,14 +798,10 @@
       </trans-unit>
       <trans-unit id="CiA4s9E" resname="set_as_default">
         <source>set_as_default</source>
         <target>حفظ الإعداد ات كمفضلة البحث</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>إلحاق كلمات مفتاحية</target>
-      </trans-unit>
       <trans-unit id="xkugSAA" resname="stats.durationFinancialYear">
         <source>stats.durationFinancialYear</source>
         <target>ساعات العمل هذه السنة المالية</target>
       </trans-unit>
       <trans-unit id="_Pym9RO" resname="stats.activeUsersFinancialYear">
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf
index cdd6bd18..4c761eab 100644
--- a/translations/messages.cs.xlf
+++ b/translations/messages.cs.xlf
@@ -884,17 +884,17 @@
       </trans-unit>
       <trans-unit id="1c0EQaz" resname="profile.registration_date">
         <source>profile.registration_date</source>
         <target>Registrován</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Připojit štítky</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Připojit</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Nahradit štítky</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Nahradit</target>
       </trans-unit>
       <trans-unit id="b43sCwO" resname="help.fixedRate">
         <source>help.fixedRate</source>
         <target>Každý záznam obdrží stejnou hodnotu, nezáleží na jeho délce</target>
       </trans-unit>
diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf
index c23e8103..9d3de43d 100644
--- a/translations/messages.da.xlf
+++ b/translations/messages.da.xlf
@@ -347,17 +347,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Farve</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Erstat etiketter</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Erstat</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Tilføj etiketter</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Tilføj</target>
       </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf
index 61d2e70c..d0990207 100644
--- a/translations/messages.de.xlf
+++ b/translations/messages.de.xlf
@@ -521,17 +521,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Farbe</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Schlagworte ersetzen</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Ersetzen</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Schlagworte hinzufügen</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Hinzufügen</target>
       </trans-unit>
       <!-- Column modal -->
       <trans-unit id="UbnqJLn" resname="modal.columns.title">
         <source>modal.columns.title</source>
         <target>Darstellung anpassen</target>
@@ -1623,8 +1623,16 @@
       </trans-unit>
       <trans-unit id="CDTC1gc" resname="supervisor">
         <source>supervisor</source>
         <target>Vorgesetzter</target>
       </trans-unit>
+      <trans-unit id=".wmq_rT" resname="force_password_change">
+        <source>force_password_change</source>
+        <target>Neues Passwort anfordern</target>
+      </trans-unit>
+      <trans-unit id="0bKwA0k" resname="force_password_change_help">
+        <source>force_password_change_help</source>
+        <target>Bei der nächsten Anmeldung muss der Benutzer ein neues Passwort hinterlegen</target>
+      </trans-unit>
     </body>
   </file>
 </xliff>
diff --git a/translations/messages.de_CH.xlf b/translations/messages.de_CH.xlf
index c92de31f..efa66f3d 100644
--- a/translations/messages.de_CH.xlf
+++ b/translations/messages.de_CH.xlf
@@ -571,17 +571,17 @@
       </trans-unit>
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
         <target>Benutzerprofil</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Schlagworte hinzufügen</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Hinzufügen</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Schlagworte ersetzen</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Ersetzen</target>
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Farbe</target>
       </trans-unit>
diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf
index 067f1eae..f4b5d901 100644
--- a/translations/messages.el.xlf
+++ b/translations/messages.el.xlf
@@ -439,17 +439,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Χρώμα</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Αντικατάσταση ετικετών</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Αντικατάσταση</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Προσάρτηση ετικετών</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Προσάρτηση</target>
       </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 42191d39..43436041 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -521,17 +521,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Color</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Replace tags</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Replace</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Append tags</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Append</target>
       </trans-unit>
       <!-- Column modal -->
       <trans-unit id="UbnqJLn" resname="modal.columns.title">
         <source>modal.columns.title</source>
         <target>Customize display</target>
@@ -1623,8 +1623,16 @@
       </trans-unit>
       <trans-unit id="CDTC1gc" resname="supervisor">
         <source>supervisor</source>
         <target>Supervisor</target>
       </trans-unit>
+      <trans-unit id=".wmq_rT" resname="force_password_change">
+        <source>force_password_change</source>
+        <target>Request new password</target>
+      </trans-unit>
+      <trans-unit id="0bKwA0k" resname="force_password_change_help">
+        <source>force_password_change_help</source>
+        <target>The next time the user logs in, they must enter a new password.</target>
+      </trans-unit>
     </body>
   </file>
 </xliff>
diff --git a/translations/messages.eo.xlf b/translations/messages.eo.xlf
index 7498e71c..5faa560a 100644
--- a/translations/messages.eo.xlf
+++ b/translations/messages.eo.xlf
@@ -371,17 +371,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Koloro</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Anstataŭigi etikedojn</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Anstataŭigi</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Postglui etikedojn</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Postglui</target>
       </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf
index d2cd3df5..aa0a316a 100644
--- a/translations/messages.es.xlf
+++ b/translations/messages.es.xlf
@@ -908,13 +908,13 @@
       </trans-unit>
       <trans-unit id="BzwWNMS" resname="status">
         <source>status</source>
         <target>Situación</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Reemplazar etiquetas</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Reemplazar</target>
       </trans-unit>
       <trans-unit id="Qc5nsFn" resname="recalculate_rates">
         <source>recalculate_rates</source>
         <target>Recalcular tarifas</target>
       </trans-unit>
@@ -956,14 +956,10 @@
       </trans-unit>
       <trans-unit id="1c0EQaz" resname="profile.registration_date">
         <source>profile.registration_date</source>
         <target>Registrado el</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Agregar etiquetas</target>
-      </trans-unit>
       <trans-unit id="b43sCwO" resname="help.fixedRate">
         <source>help.fixedRate</source>
         <target>Cada registro de tiempo obtiene el mismo valor, independientemente de su duración</target>
       </trans-unit>
       <trans-unit id="aVwNQcX" resname="billable">
diff --git a/translations/messages.eu.xlf b/translations/messages.eu.xlf
index 1f9364c8..5f2ba82d 100644
--- a/translations/messages.eu.xlf
+++ b/translations/messages.eu.xlf
@@ -935,17 +935,17 @@
       </trans-unit>
       <trans-unit id="1c0EQaz" resname="profile.registration_date">
         <source>profile.registration_date</source>
         <target>Hemen erregistratuta</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Gehitu etiketak</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Gehitu</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Ordeztu etiketak</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Ordeztu</target>
       </trans-unit>
       <trans-unit id="aVwNQcX" resname="billable">
         <source>billable</source>
         <target>Fakturagarria</target>
       </trans-unit>
diff --git a/translations/messages.fa.xlf b/translations/messages.fa.xlf
index 1a6898c5..60595ec9 100644
--- a/translations/messages.fa.xlf
+++ b/translations/messages.fa.xlf
@@ -417,18 +417,10 @@
       </trans-unit>
       <trans-unit id="b43sCwO" resname="help.fixedRate">
         <source>help.fixedRate</source>
         <target>هربار سابقه مقدار معینی میگیرد، صرف نظر از مدت آن</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>جایگزینی برچسب ها</target>
-      </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>پیوست تگ ها</target>
-      </trans-unit>
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
         <target>نمایه کاربر</target>
       </trans-unit>
       <trans-unit id="HWKJZ0T" resname="profile.about_me">
diff --git a/translations/messages.fi.xlf b/translations/messages.fi.xlf
index 69481184..5af95330 100644
--- a/translations/messages.fi.xlf
+++ b/translations/messages.fi.xlf
@@ -403,17 +403,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target state="translated">Väri</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target state="translated">Vaihda tunnisteet</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target state="translated">Vaihda</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target state="translated">Lisää tunnisteita</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target state="translated">Lisää</target>
       </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
diff --git a/translations/messages.fo.xlf b/translations/messages.fo.xlf
index 0321357c..9ac08335 100644
--- a/translations/messages.fo.xlf
+++ b/translations/messages.fo.xlf
@@ -757,17 +757,17 @@
       </trans-unit>
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
         <target>Brúkara vangi</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Legg aftrat frámerki</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Legg aftrat</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Útskift frámerki</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Útskift</target>
       </trans-unit>
       <trans-unit id="VXj0Y3h" resname="timesheet.title">
         <source>timesheet.title</source>
         <target>Mínar tíðarskrásetingar</target>
       </trans-unit>
diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf
index 0cab1842..9726170d 100644
--- a/translations/messages.fr.xlf
+++ b/translations/messages.fr.xlf
@@ -764,17 +764,17 @@
       </trans-unit>
       <trans-unit id="NNsA8i2" resname="globalsOnly">
         <source>globalsOnly</source>
         <target>Uniquement global</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Ajouter des mots-clés</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Ajouter</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Remplacer les mots-clés</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Remplacer</target>
       </trans-unit>
       <trans-unit id="b43sCwO" resname="help.fixedRate">
         <source>help.fixedRate</source>
         <target>Chaque enregistrement a la même valeur, quelle que soit sa durée</target>
       </trans-unit>
diff --git a/translations/messages.he.xlf b/translations/messages.he.xlf
index 7c4dce8c..7dea78d3 100644
--- a/translations/messages.he.xlf
+++ b/translations/messages.he.xlf
@@ -364,18 +364,10 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>צבע</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>החלפת תגיות</target>
-      </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>הוספת תגיות</target>
-      </trans-unit>
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
         <target>פרופיל משתמש</target>
       </trans-unit>
       <trans-unit id="HWKJZ0T" resname="profile.about_me">
diff --git a/translations/messages.hr.xlf b/translations/messages.hr.xlf
index 0c6662bc..7795f826 100644
--- a/translations/messages.hr.xlf
+++ b/translations/messages.hr.xlf
@@ -451,17 +451,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Boja</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Zamijeni oznake</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Zamijeni</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Dodaj oznake</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Dodaj</target>
       </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
diff --git a/translations/messages.hu.xlf b/translations/messages.hu.xlf
index 180e2702..ca0caf54 100644
--- a/translations/messages.hu.xlf
+++ b/translations/messages.hu.xlf
@@ -964,18 +964,10 @@
       </trans-unit>
       <trans-unit id="1c0EQaz" resname="profile.registration_date">
         <source>profile.registration_date</source>
         <target>Regisztrált ekkor</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Címkék megnyitása</target>
-      </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Címkék cseréje</target>
-      </trans-unit>
       <trans-unit id="b43sCwO" resname="help.fixedRate">
         <source>help.fixedRate</source>
         <target>Minden alkalommal a felvétel ugyanazt az értéket kapja, függetlenül a terjedelmétől</target>
       </trans-unit>
       <trans-unit id="J258iS6" resname="update_multiple">
diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf
index 67d64b73..fe5f42b5 100644
--- a/translations/messages.it.xlf
+++ b/translations/messages.it.xlf
@@ -403,17 +403,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Colore</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Sostituisci etichette</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Sostituisci</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Aggiungi etichette</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Aggiungi</target>
       </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
diff --git a/translations/messages.ko.xlf b/translations/messages.ko.xlf
index 03774567..d1e8a0cc 100644
--- a/translations/messages.ko.xlf
+++ b/translations/messages.ko.xlf
@@ -848,18 +848,10 @@
       </trans-unit>
       <trans-unit id="gRIArHS" resname="lastLogin">
         <source>lastLogin</source>
         <target>지난 로그인</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>태그 추가</target>
-      </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>태그 교체</target>
-      </trans-unit>
       <trans-unit id="1c0EQaz" resname="profile.registration_date">
         <source>profile.registration_date</source>
         <target>에 등록</target>
       </trans-unit>
       <trans-unit id="tfkKlua" resname="progress">
diff --git a/translations/messages.nb_NO.xlf b/translations/messages.nb_NO.xlf
index b0e1fc0c..51eb901e 100644
--- a/translations/messages.nb_NO.xlf
+++ b/translations/messages.nb_NO.xlf
@@ -232,14 +232,10 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Farge</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Erstatt etiketter</target>
-      </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
@@ -622,14 +618,10 @@
       </trans-unit>
       <trans-unit id="_0tuMNE" resname="fixedRate">
         <source>fixedRate</source>
         <target>Fast sats</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Legg til etiketter</target>
-      </trans-unit>
       <trans-unit id="j4Kd9N1" resname="calendar_initial_view">
         <source>calendar_initial_view</source>
         <target>Startvisning for kalender</target>
       </trans-unit>
       <trans-unit id="H.IqJTQ" resname="login_initial_view">
diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf
index cbff6117..36266906 100644
--- a/translations/messages.nl.xlf
+++ b/translations/messages.nl.xlf
@@ -940,18 +940,10 @@
       </trans-unit>
       <trans-unit id="gRIArHS" resname="lastLogin">
         <source>lastLogin</source>
         <target>Laatste login</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Voeg labels toe</target>
-      </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Vervang labels</target>
-      </trans-unit>
       <trans-unit id="b43sCwO" resname="help.fixedRate">
         <source>help.fixedRate</source>
         <target state="translated">Elke tijdsinvoer krijgt dezelfde prijs, ongeacht de duur ervan</target>
       </trans-unit>
       <trans-unit id="Qc5nsFn" resname="recalculate_rates">
diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf
index 49ade746..0cf5435a 100644
--- a/translations/messages.pl.xlf
+++ b/translations/messages.pl.xlf
@@ -359,17 +359,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Kolor</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Zamień tagi</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Zamień</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Dodaj tagi</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Dodaj</target>
       </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
diff --git a/translations/messages.pt.xlf b/translations/messages.pt.xlf
index f898cfe4..a64d1fcc 100644
--- a/translations/messages.pt.xlf
+++ b/translations/messages.pt.xlf
@@ -860,17 +860,17 @@
       </trans-unit>
       <trans-unit id="gRIArHS" resname="lastLogin">
         <source>lastLogin</source>
         <target>Última sessão</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Anexar etiquetas</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Anexar</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Substituir etiquetas</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Substituir</target>
       </trans-unit>
       <trans-unit id="b43sCwO" resname="help.fixedRate">
         <source>help.fixedRate</source>
         <target>Cada registo de tempo obtém o mesmo valor, independentemente da sua duração</target>
       </trans-unit>
diff --git a/translations/messages.pt_BR.xlf b/translations/messages.pt_BR.xlf
index a75b35b6..844a21b3 100644
--- a/translations/messages.pt_BR.xlf
+++ b/translations/messages.pt_BR.xlf
@@ -804,14 +804,10 @@
       </trans-unit>
       <trans-unit id="J258iS6" resname="update_multiple">
         <source>update_multiple</source>
         <target>%action% %count% entradas?</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Tags de apêndice</target>
-      </trans-unit>
       <trans-unit id="E7G60OF" resname="Allowed character: A-Z and _">
         <source>Allowed character: A-Z and _</source>
         <target>Caractere permitido: A-Z e _</target>
       </trans-unit>
       <trans-unit id="EJDS9HY" resname="vat_id">
@@ -928,14 +924,10 @@
       </trans-unit>
       <trans-unit id="1c0EQaz" resname="profile.registration_date">
         <source>profile.registration_date</source>
         <target>Registrado em</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Substituir tags</target>
-      </trans-unit>
       <trans-unit id="tfkKlua" resname="progress">
         <source>progress</source>
         <target>Progresso</target>
       </trans-unit>
       <trans-unit id="L05Qw2x" resname="my_teams">
diff --git a/translations/messages.ro.xlf b/translations/messages.ro.xlf
index 467f9693..a6dc6fe1 100644
--- a/translations/messages.ro.xlf
+++ b/translations/messages.ro.xlf
@@ -344,18 +344,10 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Culoare</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Înlocuiește etichetele</target>
-      </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Adaugă etichete</target>
-      </trans-unit>
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
         <target>Profilul utilizatorului</target>
       </trans-unit>
       <trans-unit id="HWKJZ0T" resname="profile.about_me">
diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf
index aee63964..72f6b93b 100644
--- a/translations/messages.ru.xlf
+++ b/translations/messages.ru.xlf
@@ -710,17 +710,17 @@
       </trans-unit>
       <trans-unit id="1c0EQaz" resname="profile.registration_date">
         <source>profile.registration_date</source>
         <target state="translated">Зарегистрирован(-а)</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target state="translated">Добавить метки</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target state="translated">Добавить</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target state="translated">Заменить метки</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target state="translated">Заменить</target>
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Цвет</target>
       </trans-unit>
diff --git a/translations/messages.sk.xlf b/translations/messages.sk.xlf
index bad06fb0..1440b527 100644
--- a/translations/messages.sk.xlf
+++ b/translations/messages.sk.xlf
@@ -355,17 +355,17 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Farba</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Zmeniť značky</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Zmeniť</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Pridať značky</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Pridať</target>
       </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
diff --git a/translations/messages.sv.xlf b/translations/messages.sv.xlf
index dc45c3e4..9f4a1cd2 100644
--- a/translations/messages.sv.xlf
+++ b/translations/messages.sv.xlf
@@ -936,17 +936,17 @@
       </trans-unit>
       <trans-unit id="dherSTU" resname="export_decimal">
         <source>export_decimal</source>
         <target>Använd decimallängd vid export</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Fäst taggar</target>
+      <trans-unit id="ICpl9sx" resname="append">
+        <source>append</source>
+        <target>Fäst</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Ersätt taggar</target>
+      <trans-unit id="tl01EdS" resname="replace">
+        <source>replace</source>
+        <target>Ersätt</target>
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Färg</target>
       </trans-unit>
diff --git a/translations/messages.tr.xlf b/translations/messages.tr.xlf
index 4c9905c3..d3548276 100644
--- a/translations/messages.tr.xlf
+++ b/translations/messages.tr.xlf
@@ -904,18 +904,10 @@
       </trans-unit>
       <trans-unit id="1c0EQaz" resname="profile.registration_date">
         <source>profile.registration_date</source>
         <target>Kayıt tarihi</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Etiketleri ekle</target>
-      </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Etiketleri değiştir</target>
-      </trans-unit>
       <trans-unit id="b43sCwO" resname="help.fixedRate">
         <source>help.fixedRate</source>
         <target state="translated">Süresi ne olursa olsun her kayıt aynı fiyatı alır</target>
       </trans-unit>
       <trans-unit id="aVwNQcX" resname="billable">
diff --git a/translations/messages.uk.xlf b/translations/messages.uk.xlf
index 2335d0dd..80773af1 100644
--- a/translations/messages.uk.xlf
+++ b/translations/messages.uk.xlf
@@ -340,18 +340,10 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target state="translated">Колір</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target state="translated">Заміна міток</target>
-      </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target state="translated">Додати мітки</target>
-      </trans-unit>
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
         <target state="translated">Профіль користувача</target>
       </trans-unit>
       <trans-unit id="HWKJZ0T" resname="profile.about_me">
diff --git a/translations/messages.vi.xlf b/translations/messages.vi.xlf
index 36712f07..c2c9bb86 100644
--- a/translations/messages.vi.xlf
+++ b/translations/messages.vi.xlf
@@ -387,18 +387,10 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>Màu sắc</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>Thay thế thẻ</target>
-      </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>Nối các thẻ</target>
-      </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
diff --git a/translations/messages.zh_CN.xlf b/translations/messages.zh_CN.xlf
index c36a5482..ec85e51e 100644
--- a/translations/messages.zh_CN.xlf
+++ b/translations/messages.zh_CN.xlf
@@ -347,18 +347,10 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target>颜色</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target>替换标签</target>
-      </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target>附加标签</target>
-      </trans-unit>
       <!--
               User profile
             -->
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
diff --git a/translations/messages.zh_Hant.xlf b/translations/messages.zh_Hant.xlf
index 851d4eb3..50f1747c 100644
--- a/translations/messages.zh_Hant.xlf
+++ b/translations/messages.zh_Hant.xlf
@@ -695,14 +695,10 @@
       </trans-unit>
       <trans-unit id="ygtTz8." resname="profile.roles">
         <source>profile.roles</source>
         <target state="translated">角色</target>
       </trans-unit>
-      <trans-unit id="tl01EdS" resname="replaceTags">
-        <source>replaceTags</source>
-        <target state="translated">替換 tags</target>
-      </trans-unit>
       <trans-unit id="HWKJZ0T" resname="profile.about_me">
         <source>profile.about_me</source>
         <target state="translated">關於我</target>
       </trans-unit>
       <trans-unit id="A6TLLQa" resname="profile.password">
@@ -719,14 +715,10 @@
       </trans-unit>
       <trans-unit id="dChNncv" resname="color">
         <source>color</source>
         <target state="translated">色彩</target>
       </trans-unit>
-      <trans-unit id="ICpl9sx" resname="appendTags">
-        <source>appendTags</source>
-        <target state="translated">添加 tags</target>
-      </trans-unit>
       <trans-unit id="1_wnK76" resname="profile.title">
         <source>profile.title</source>
         <target state="translated">使用者個人資料</target>
       </trans-unit>
       <trans-unit id="OWLt7pw" resname="timezone">
diff --git a/translations/wizard.de.xlf b/translations/wizard.de.xlf
index 9f36a9d2..d7fcd991 100644
--- a/translations/wizard.de.xlf
+++ b/translations/wizard.de.xlf
@@ -20,10 +20,18 @@
       </trans-unit>
       <trans-unit id="Mo1506M" resname="wizard.profile.description">
         <source>wizard.profile.description</source>
         <target>Einige Grundeinstellungen um Ihnen die Arbeit mit Kimai zu erleichtern.</target>
       </trans-unit>
+      <trans-unit id="7Vipfs7" resname="wizard.password.title">
+        <source>wizard.password.title</source>
+        <target>Ihr Passwort</target>
+      </trans-unit>
+      <trans-unit id="zlNbbUg" resname="wizard.password.description">
+        <source>wizard.password.description</source>
+        <target>Bitte geben Sie ihr neues Passwort ein, das von nun an für die Anmeldung gelten soll.</target>
+      </trans-unit>
       <trans-unit id="8p.2pXa" resname="wizard.done.title">
         <source>wizard.done.title</source>
         <target>Herzlichen Glückwunsch</target>
       </trans-unit>
       <trans-unit id="k1SzMuI" resname="wizard.done.description">
diff --git a/translations/wizard.en.xlf b/translations/wizard.en.xlf
index f8345847..792f7834 100644
--- a/translations/wizard.en.xlf
+++ b/translations/wizard.en.xlf
@@ -20,10 +20,18 @@
       </trans-unit>
       <trans-unit id="Mo1506M" resname="wizard.profile.description">
         <source>wizard.profile.description</source>
         <target>Some basic settings to make your work with Kimai easier.</target>
       </trans-unit>
+      <trans-unit id="7Vipfs7" resname="wizard.password.title">
+        <source>wizard.password.title</source>
+        <target>Your password</target>
+      </trans-unit>
+      <trans-unit id="zlNbbUg" resname="wizard.password.description">
+        <source>wizard.password.description</source>
+        <target>Please enter your new password, which will be used for logging in from now on.</target>
+      </trans-unit>
       <trans-unit id="8p.2pXa" resname="wizard.done.title">
         <source>wizard.done.title</source>
         <target>Congratulations</target>
       </trans-unit>
       <trans-unit id="k1SzMuI" resname="wizard.done.description">

Vulnerable code: .htaccess
.env-*
.idea/
.DS_Store
rector.php
phpstan.sh
public/.user.ini
php.ini
.php-version
.yarnrc.yml
.yarn
!.gitkeep

/bin/*
!/bin/console
*local.yaml

/templates/invoice/renderer/.~lock*
/translations/branding.*.xlf

/var/coverage/

/var/invoices/*
*.disabled
        "kevinpapst/tabler-bundle": "^0.22",
    "content-hash": "9159cc3cf1d749c412d1353e921de6fc",
            "version": "2.1.3",
                "reference": "3023e150f90a38843856147b58190aa8b46cc155"
                "url": "https://api.github.com/repos/doctrine/collections/zipball/3023e150f90a38843856147b58190aa8b46cc155",
                "reference": "3023e150f90a38843856147b58190aa8b46cc155",
                "doctrine/coding-standard": "^10.0",
                "source": "https://github.com/doctrine/collections/tree/2.1.3"
            "time": "2023-07-06T15:15:36+00:00"
            "version": "3.6.6",
                "reference": "63646ffd71d1676d2f747f871be31b7e921c7864"
                "url": "https://api.github.com/repos/doctrine/dbal/zipball/63646ffd71d1676d2f747f871be31b7e921c7864",
                "reference": "63646ffd71d1676d2f747f871be31b7e921c7864",
                "phpstan/phpstan": "1.10.29",
                "phpunit/phpunit": "9.6.9",
                "source": "https://github.com/doctrine/dbal/tree/3.6.6"
            "time": "2023-08-17T05:38:17+00:00"
            "version": "v1.1.1",
                "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3"
                "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
                "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
                "source": "https://github.com/doctrine/deprecations/tree/v1.1.1"
            "time": "2023-06-03T09:27:29+00:00"
            "version": "4.0.1",
                "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff"
                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/3a85486b709bc384dae8eb78fb2eec649bdb64ff",
                "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff",
                "phpunit/phpunit": "^9.5.27",
                "vimeo/psalm": "^4.30"
                "source": "https://github.com/egulias/EmailValidator/tree/4.0.1"
            "time": "2023-01-14T14:17:03+00:00"
            "version": "4.8.4",
                "reference": "a122b85d4a5a3257d471257a43ac3e5676a27ffe"
                "url": "https://api.github.com/repos/endroid/qr-code/zipball/a122b85d4a5a3257d471257a43ac3e5676a27ffe",
                "reference": "a122b85d4a5a3257d471257a43ac3e5676a27ffe",
                "source": "https://github.com/endroid/qr-code/tree/4.8.4"
            "time": "2023-08-28T18:12:07+00:00"
            "version": "3.5.0",
                "reference": "893f3a01e4d88789abc399c6f1b3cfff79238734"
                "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/893f3a01e4d88789abc399c6f1b3cfff79238734",
                "reference": "893f3a01e4d88789abc399c6f1b3cfff79238734",
                "symfony/config": "^4.4|^5.3|^6.0",
                "symfony/dependency-injection": "^4.4|^5.3|^6.0",
                "symfony/event-dispatcher": "^4.4|^5.3|^6.0",
                "symfony/http-foundation": "^4.4|^5.3|^6.0",
                "symfony/http-kernel": "^4.4|^5.3|^6.0",
                "symfony/routing": "^4.4|^5.3|^6.0",
                "symfony/security-core": "^4.4|^5.3|^6.0",
                "sensio/framework-extra-bundle": "<6.1",
                "symfony/error-handler": "<4.4.1"
                "doctrine/annotations": "^1.13.2",
                "symfony/asset": "^4.4|^5.3|^6.0",
                "symfony/browser-kit": "^4.4|^5.3|^6.0",
                "symfony/css-selector": "^4.4|^5.3|^6.0",
                "symfony/expression-language": "^4.4|^5.3|^6.0",
                "symfony/form": "^4.4|^5.3|^6.0",
                "symfony/mime": "^4.4|^5.3|^6.0",
                "symfony/phpunit-bridge": "^5.3|^6.0",
                "symfony/security-bundle": "^4.4|^5.3|^6.0",
                "symfony/serializer": "^4.4|^5.3|^6.0",
                "symfony/twig-bundle": "^4.4|^5.3|^6.0",
                "symfony/validator": "^4.4|^5.3|^6.0",
                "symfony/web-profiler-bundle": "^4.4|^5.3|^6.0",
                "symfony/yaml": "^4.4|^5.3|^6.0"
                "source": "https://github.com/FriendsOfSymfony/FOSRestBundle/tree/3.5.0"
            "time": "2023-01-06T15:33:47+00:00"
            "version": "0.22",
                "reference": "fdc3c182f13dec439ce5110dededba2895518f8f"
                "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/fdc3c182f13dec439ce5110dededba2895518f8f",
                "reference": "fdc3c182f13dec439ce5110dededba2895518f8f",
                "php": ">=7.4",
                "symfony/asset": "^4.4 || ^5.4 || ^6.0",
                "symfony/config": "^4.4 || ^5.4 || ^6.0",
                "symfony/dependency-injection": "^4.4 || ^5.4 || ^6.0",
                "symfony/event-dispatcher": "^4.4 || ^5.4 || ^6.0",
                "symfony/http-foundation": "^4.4 || ^5.4 || ^6.0",
                "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0",
                "symfony/options-resolver": "^4.4 || ^5.4 || ^6.0",
                "symfony/security-core": "^4.4 || ^5.4 || ^6.0",
                "symfony/translation": "^4.4 || ^5.4 || ^6.0",
                "symfony/twig-bridge": "^4.4 || ^5.4 || ^6.0",
                "phpspec/prophecy": "^1.6",
                "symfony/framework-bundle": "^4.4 || ^5.4 || ^6.0"
                "source": "https://github.com/kevinpapst/TablerBundle/tree/0.22"
            "time": "2023-06-15T06:00:11+00:00"
            "version": "2.12.0",
                "reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490"
                "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490",
                "reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490",
                "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0"
                "infection/infection": "^0.26.6",
                "laminas/laminas-coding-standard": "~2.4.0",
                "phpunit/phpunit": "^9.5.18",
                "psalm/plugin-phpunit": "^0.17.0",
                "vimeo/psalm": "^4.22.0"
            "time": "2022-10-10T10:11:09+00:00"
            "version": "9.10.0",
                "reference": "d24b0d484812313b07ab74b0fe4db9661606df6c"
                "url": "https://api.github.com/repos/thephpleague/csv/zipball/d24b0d484812313b07ab74b0fe4db9661606df6c",
                "reference": "d24b0d484812313b07ab74b0fe4db9661606df6c",
            "time": "2023-08-04T15:12:48+00:00"
            "version": "2.0.0",
                "reference": "1cf4c0b68b8461cea27411ff961482ce7687e34f"
                "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/1cf4c0b68b8461cea27411ff961482ce7687e34f",
                "reference": "1cf4c0b68b8461cea27411ff961482ce7687e34f",
                "source": "https://github.com/mpdf/psr-http-message-shim/tree/2.0.0"
            "time": "2023-09-01T06:08:18+00:00"
            "version": "1.24.1",
                "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01"
                "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01",
                "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01",
                "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.1"
            "time": "2023-09-18T12:18:02+00:00"
            "version": "1.0.2",
                "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31"
                "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31",
                "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31",
                "source": "https://github.com/php-fig/http-client/tree/1.0.2"
            "time": "2023-04-10T20:12:12+00:00"
            "version": "v2.4.1",
                "reference": "f4ba73e5bc053ccc90b81717c5df1cb2ea7bae7b"
                "url": "https://api.github.com/repos/Setasign/FPDI/zipball/f4ba73e5bc053ccc90b81717c5df1cb2ea7bae7b",
                "reference": "f4ba73e5bc053ccc90b81717c5df1cb2ea7bae7b",
                "source": "https://github.com/Setasign/FPDI/tree/v2.4.1"
            "time": "2023-07-27T08:12:09+00:00"
            "version": "v6.3.4",
                "reference": "e60d00b4f633efa4c1ef54e77c12762d9073e7b3"
                "url": "https://api.github.com/repos/symfony/cache/zipball/e60d00b4f633efa4c1ef54e77c12762d9073e7b3",
                "reference": "e60d00b4f633efa4c1ef54e77c12762d9073e7b3",
                "source": "https://github.com/symfony/cache/tree/v6.3.4"
            "time": "2023-08-05T09:10:27+00:00"
            "version": "v6.3.4",
                "reference": "68a5a9570806a087982f383f6109c5e925892a49"
                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/68a5a9570806a087982f383f6109c5e925892a49",
                "reference": "68a5a9570806a087982f383f6109c5e925892a49",
                "source": "https://github.com/symfony/dependency-injection/tree/v6.3.4"
            "time": "2023-08-16T17:55:17+00:00"
            "version": "v6.3.4",
                "reference": "589eeeb93669739ec1d8bd4593e4972d94e0981d"
                "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/589eeeb93669739ec1d8bd4593e4972d94e0981d",
                "reference": "589eeeb93669739ec1d8bd4593e4972d94e0981d",
                "source": "https://github.com/symfony/doctrine-bridge/tree/v6.3.4"
            "time": "2023-08-08T10:40:25+00:00"
            "version": "v6.3.2",
                "reference": "85fd65ed295c4078367c784e8a5a6cee30348b7a"
                "url": "https://api.github.com/repos/symfony/error-handler/zipball/85fd65ed295c4078367c784e8a5a6cee30348b7a",
                "reference": "85fd65ed295c4078367c784e8a5a6cee30348b7a",
                "source": "https://github.com/symfony/error-handler/tree/v6.3.2"
            "time": "2023-07-16T17:05:46+00:00"
            "version": "v6.3.3",
                "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e"
                "url": "https://api.github.com/repos/symfony/finder/zipball/9915db259f67d21eefee768c1abcf1cc61b1fc9e",
                "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e",
                "source": "https://github.com/symfony/finder/tree/v6.3.3"
            "time": "2023-07-31T08:31:44+00:00"
            "version": "v6.3.2",
                "reference": "afdadf511e08bc6d4752afb869ce084276aca4e2"
                "url": "https://api.github.com/repos/symfony/form/zipball/afdadf511e08bc6d4752afb869ce084276aca4e2",
                "reference": "afdadf511e08bc6d4752afb869ce084276aca4e2",
                "source": "https://github.com/symfony/form/tree/v6.3.2"
            "time": "2023-07-26T17:39:03+00:00"
            "version": "v6.3.4",
                "reference": "f822f54ff05cd88878910b4559f66c12176d952c"
                "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/f822f54ff05cd88878910b4559f66c12176d952c",
                "reference": "f822f54ff05cd88878910b4559f66c12176d952c",
                "source": "https://github.com/symfony/framework-bundle/tree/v6.3.4"
            "time": "2023-08-16T18:04:38+00:00"
            "version": "v6.3.2",
                "reference": "15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00"
                "url": "https://api.github.com/repos/symfony/http-client/zipball/15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00",
                "reference": "15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00",
                "source": "https://github.com/symfony/http-client/tree/v6.3.2"
            "time": "2023-07-05T08:41:27+00:00"
            "version": "v6.3.4",
                "reference": "cac1556fdfdf6719668181974104e6fcfa60e844"
                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/cac1556fdfdf6719668181974104e6fcfa60e844",
                "reference": "cac1556fdfdf6719668181974104e6fcfa60e844",
                "source": "https://github.com/symfony/http-foundation/tree/v6.3.4"
            "time": "2023-08-22T08:20:46+00:00"
            "version": "v6.3.4",
                "reference": "36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb"
                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb",
                "reference": "36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb",
                "source": "https://github.com/symfony/http-kernel/tree/v6.3.4"
            "time": "2023-08-26T13:54:49+00:00"
            "version": "v6.3.0",
                "reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435"
                "url": "https://api.github.com/repos/symfony/mailer/zipball/7b03d9be1dea29bfec0a6c7b603f5072a4c97435",
                "reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435",
                "source": "https://github.com/symfony/mailer/tree/v6.3.0"
            "time": "2023-05-29T12:49:39+00:00"
            "version": "v6.3.3",
                "reference": "9a0cbd52baa5ba5a5b1f0cacc59466f194730f98"
                "url": "https://api.github.com/repos/symfony/mime/zipball/9a0cbd52baa5ba5a5b1f0cacc59466f194730f98",
                "reference": "9a0cbd52baa5ba5a5b1f0cacc59466f194730f98",
                "source": "https://github.com/symfony/mime/tree/v6.3.3"
            "time": "2023-07-31T07:08:24+00:00"
            "version": "v6.3.0",
                "reference": "d23ad221989e6b8278d050cabfd7b569eee84590"
                "url": "https://api.github.com/repos/symfony/password-hasher/zipball/d23ad221989e6b8278d050cabfd7b569eee84590",
                "reference": "d23ad221989e6b8278d050cabfd7b569eee84590",
                "source": "https://github.com/symfony/password-hasher/tree/v6.3.0"
            "time": "2023-02-14T09:04:20+00:00"
        },
        {
            "name": "symfony/polyfill-ctype",
            "version": "v1.28.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-ctype.git",
                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
                "shasum": ""
            },
            "require": {
                "php": ">=7.1"
            },
            "provide": {
                "ext-ctype": "*"
            },
            "suggest": {
                "ext-ctype": "For best performance"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-main": "1.28-dev"
                },
                "thanks": {
                    "name": "symfony/polyfill",
                    "url": "https://github.com/symfony/polyfill"
                }
            },
            "autoload": {
                "files": [
                    "bootstrap.php"
                ],
                "psr-4": {
                    "Symfony\\Polyfill\\Ctype\\": ""
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Gert de Pagter",
                    "email": "BackEndTea@gmail.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Symfony polyfill for ctype functions",
            "homepage": "https://symfony.com",
            "keywords": [
                "compatibility",
                "ctype",
                "polyfill",
                "portable"
            ],
            "support": {
                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.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": "2023-01-26T09:26:14+00:00"
            "version": "v6.3.3",
                "reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a"
                "url": "https://api.github.com/repos/symfony/routing/zipball/e7243039ab663822ff134fbc46099b5fdfa16f6a",
                "reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a",
                "source": "https://github.com/symfony/routing/tree/v6.3.3"
            "time": "2023-07-31T07:08:24+00:00"
            "version": "v6.3.4",
                "reference": "31957477b289220a47880ead3727bf5cc059fa08"
                "url": "https://api.github.com/repos/symfony/security-bundle/zipball/31957477b289220a47880ead3727bf5cc059fa08",
                "reference": "31957477b289220a47880ead3727bf5cc059fa08",
                "symfony/security-http": "^6.3"
                "source": "https://github.com/symfony/security-bundle/tree/v6.3.4"
            "time": "2023-08-25T08:46:23+00:00"
            "version": "v6.3.3",
                "reference": "b86ce012cc9a62a15ed43af5037eebc3e6de4d7f"
                "url": "https://api.github.com/repos/symfony/security-core/zipball/b86ce012cc9a62a15ed43af5037eebc3e6de4d7f",
                "reference": "b86ce012cc9a62a15ed43af5037eebc3e6de4d7f",
                "source": "https://github.com/symfony/security-core/tree/v6.3.3"
            "time": "2023-07-31T07:08:24+00:00"
            "version": "v6.3.4",
                "reference": "0afb37c1120c1c46219bdbd1dd912fb4d48eaf7d"
                "url": "https://api.github.com/repos/symfony/security-http/zipball/0afb37c1120c1c46219bdbd1dd912fb4d48eaf7d",
                "reference": "0afb37c1120c1c46219bdbd1dd912fb4d48eaf7d",
                "source": "https://github.com/symfony/security-http/tree/v6.3.4"
            "time": "2023-08-25T19:43:09+00:00"
            "version": "v6.3.4",
                "reference": "96d28a58d5a128bf77c54534b380eb7c92c8f846"
                "url": "https://api.github.com/repos/symfony/serializer/zipball/96d28a58d5a128bf77c54534b380eb7c92c8f846",
                "reference": "96d28a58d5a128bf77c54534b380eb7c92c8f846",
                "source": "https://github.com/symfony/serializer/tree/v6.3.4"
            "time": "2023-08-24T14:35:28+00:00"
            "version": "v6.3.2",
                "reference": "53d1a83225002635bca3482fcbf963001313fb68"
                "url": "https://api.github.com/repos/symfony/string/zipball/53d1a83225002635bca3482fcbf963001313fb68",
                "reference": "53d1a83225002635bca3482fcbf963001313fb68",
                "source": "https://github.com/symfony/string/tree/v6.3.2"
            "time": "2023-07-05T08:41:27+00:00"
            "version": "v6.3.2",
                "reference": "6f8435db76a2d79917489a19a82679276c1b4e32"
                "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/6f8435db76a2d79917489a19a82679276c1b4e32",
                "reference": "6f8435db76a2d79917489a19a82679276c1b4e32",
                "source": "https://github.com/symfony/twig-bridge/tree/v6.3.2"
            "time": "2023-07-20T16:42:33+00:00"
            "version": "v6.3.4",
                "reference": "0c8435154920b9bbe93bece675234c244cadf73b"
                "url": "https://api.github.com/repos/symfony/validator/zipball/0c8435154920b9bbe93bece675234c244cadf73b",
                "reference": "0c8435154920b9bbe93bece675234c244cadf73b",
                "source": "https://github.com/symfony/validator/tree/v6.3.4"
            "time": "2023-08-17T15:49:05+00:00"
            "version": "v6.3.4",
                "reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45"
                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2027be14f8ae8eae999ceadebcda5b4909b81d45",
                "reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45",
                "source": "https://github.com/symfony/var-dumper/tree/v6.3.4"
            "time": "2023-08-24T14:51:05+00:00"
            "version": "v2.0.1",
                "reference": "150fe022740fef908f4ca3d5950ce85ab040ec76"
                "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/150fe022740fef908f4ca3d5950ce85ab040ec76",
                "reference": "150fe022740fef908f4ca3d5950ce85ab040ec76",
                "symfony/asset": "^5.4 || ^6.2",
                "symfony/config": "^5.4 || ^6.2",
                "symfony/dependency-injection": "^5.4 || ^6.2",
                "symfony/http-kernel": "^5.4 || ^6.2",
                "symfony/framework-bundle": "^5.4 || ^6.2",
                "symfony/phpunit-bridge": "^5.4 || ^6.2",
                "symfony/twig-bundle": "^5.4 || ^6.2",
                "symfony/web-link": "^5.4 || ^6.2"
                "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.0.1"
            "time": "2023-05-31T14:28:33+00:00"
            "version": "4.7.14",
                "reference": "e53c0c7a6e250c435cc13ab50792247a8eb07da3"
                "url": "https://api.github.com/repos/zircote/swagger-php/zipball/e53c0c7a6e250c435cc13ab50792247a8eb07da3",
                "reference": "e53c0c7a6e250c435cc13ab50792247a8eb07da3",
                "source": "https://github.com/zircote/swagger-php/tree/4.7.14"
            "time": "2023-09-12T00:18:13+00:00"
            "version": "3.1.0",
                "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2"
                "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2",
                "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2",
                "source": "https://github.com/composer/pcre/tree/3.1.0"
            "time": "2022-11-17T09:50:14+00:00"
            "version": "v3.27.0",
                "reference": "e73ccaae1208f017bb7860986eebb3da48bd25d6"
                "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/e73ccaae1208f017bb7860986eebb3da48bd25d6",
                "reference": "e73ccaae1208f017bb7860986eebb3da48bd25d6",
                "phpunitgoodpractices/polyfill": "^1.6",
                "phpunitgoodpractices/traits": "^1.9.2",
                "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.27.0"
            "time": "2023-09-17T14:37:54+00:00"
            "version": "1.10.34",
                "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901"
                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7f806b6f1403e6914c778140e2ba07c293cb4901",
                "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901",
            "time": "2023-09-13T09:49:47+00:00"
            "version": "1.3.14",
                "reference": "614acc10c522e319639bf38b0698a4a566665f04"
                "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/614acc10c522e319639bf38b0698a4a566665f04",
                "reference": "614acc10c522e319639bf38b0698a4a566665f04",
                "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.14"
            "time": "2023-08-25T09:46:39+00:00"
            "version": "1.3.2",
                "reference": "7332b90dfc291ac5b4b83fbca2081936faa1e3f9"
                "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/7332b90dfc291ac5b4b83fbca2081936faa1e3f9",
                "reference": "7332b90dfc291ac5b4b83fbca2081936faa1e3f9",
                "phpstan/phpstan": "^1.9.18"
                "phpstan/phpstan-phpunit": "^1.0",
                "phpstan/phpstan-strict-rules": "^1.0",
                "symfony/serializer": "^5.4"
                "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.2"
            "time": "2023-05-16T12:46:15+00:00"
            "version": "9.2.28",
                "reference": "7134a5ccaaf0f1c92a4f5501a6c9f98ac4dcc0ef"
                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7134a5ccaaf0f1c92a4f5501a6c9f98ac4dcc0ef",
                "reference": "7134a5ccaaf0f1c92a4f5501a6c9f98ac4dcc0ef",
                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.28"
            "time": "2023-09-12T14:36:20+00:00"
    #esi: ~
    #fragments: ~
#    invoice:
#        # all files in these directories will be used as invoice documents (if supported by a renderer)
#        documents:
#            - 'var/invoices/'
#            - 'templates/invoice/renderer/'
    type: annotation
    type: annotation
    type: annotation
    type: annotation
    type: annotation
        tags:
            - { name: doctrine.event_subscriber, priority: 50 }
    Gedmo\Timestampable\TimestampableListener:
        class: Gedmo\Timestampable\TimestampableListener
        tags:
            - { name: doctrine.event_subscriber, priority: 60 }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]
            count: 2
            count: 3
            count: 6
            count: 3
            count: 3
            count: 2
            count: 3
        -
            message: "#^Method App\\\\Utils\\\\MenuItemModel\\:\\:getRouteArgs\\(\\) return type has no value type specified in iterable type array\\.$#"
            count: 1
            path: src/Utils/MenuItemModel.php

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
     * @template TData of mixed
     * @param TData|null $data
     * @return FormInterface<TData|null>
    protected function createSearchForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
    #[Rest\QueryParam(name: 'full', strict: true, nullable: true, description: 'Allows to fetch fully serialized objects including subresources. Allowed values: true (default: false)')]
        if (null !== $paramFetcher->get('full')) {
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
        $view->getContext()->setGroups(self::GROUPS_COLLECTION);
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
    /**
     * {@inheritdoc}
     */
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
    public const VERSION = '2.0.35';
    public const VERSION_ID = 20035;
     * The Github repository name
use Symfony\Component\Form\Extension\Core\Type\FormType;
     * @template TData of mixed
     * @param TData|null $data
     * @return FormInterface<TData|null>
    protected function createFormWithName(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
    #[Route(path: '/document_reload/{document}', name: 'admin_invoice_document_reload', methods: ['GET', 'POST'])]
    #[IsGranted('upload_invoice_template')]
    public function reloadDocument(string $document, Environment $twig): Response
    {
        $event = new InvoiceDocumentsEvent($this->service->getDocuments(true));
        $this->dispatcher->dispatch($event);

        $reloaded = false;

        foreach ($event->getInvoiceDocuments() as $doc) {
            if ($document === $doc->getId() && $doc->isTwig()) {
                $reloaded = true;
                try {
                    $twig->enableAutoReload();
                    $twig->load('@invoice/' . basename($doc->getFilename()));
                    $twig->disableAutoReload();
                    $this->flashSuccess('Reloaded template');
                } catch (Exception $ex) {
                    $this->flashException($ex, 'Failed to reload template: ' . $ex->getMessage());
                }
            }
        }

        if (!$reloaded) {
            throw $this->createNotFoundException('Unknown document: ' . $document);
        }

        return $this->redirectToRoute('admin_invoice_document_upload');
    }

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
        $content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
        $content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
use Exception;
    /**
     * @param Request $request
     * @return Response
     * @throws Exception
     */
    /**
     * @param Request $request
     * @return Response
     * @throws Exception
     */
        $content = $this->container->get('twig')->render('reporting/report_user_list_monthly_export.html.twig', $data);
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
            'tokenLifetime' => ceil($this->configuration->getPasswordResetRetryLifetime() / 3600),
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
                    return $this->redirectToRoute('wizard', ['wizard' => 'done', '_locale' => $data['language']]);
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
                    ->defaultTrue()
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
    #[ORM\OneToMany(targetEntity: ActivityMeta::class, mappedBy: 'activity', cascade: ['persist'])]
    #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'activities')]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\OneToMany(targetEntity: CustomerMeta::class, mappedBy: 'customer', cascade: ['persist'])]
    #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'customers')]
    #[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\OneToMany(targetEntity: InvoiceMeta::class, mappedBy: 'invoice', cascade: ['persist'])]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\OneToMany(targetEntity: ProjectMeta::class, mappedBy: 'project', cascade: ['persist'])]
    #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'projects')]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[Assert\Regex(pattern: '/,/', match: false, message: 'Tag name cannot contain comma')]
    #[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'team', fetch: 'LAZY', cascade: ['persist', 'remove'], orphanRemoval: true)]
    #[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
    #[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
    #[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
use Gedmo\Mapping\Annotation as Gedmo;
    #[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    /**
     * @internal used for limiting queries, eg. via API sync
     */
    #[Gedmo\Timestampable]
    private ?\DateTime $modifiedAt = null;
    #[ORM\OneToMany(targetEntity: TimesheetMeta::class, mappedBy: 'timesheet', cascade: ['persist'])]
    public function getModifiedAt(): ?DateTime
        $this->modifiedAt = null;
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Exporter\Expose(name: 'last_login', label: 'lastLogin', exp: 'object.getLastLogin()', type: 'datetime')]
#[Exporter\Expose(name: 'roles', label: 'roles', exp: 'object.getRoles()', type: 'array')]
#[Exporter\Expose(name: 'active', label: 'active', exp: 'object.isEnabled()', type: 'boolean')]
    #[ORM\OneToMany(targetEntity: UserPreference::class, mappedBy: 'user', cascade: ['persist'])]
    #[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'user', fetch: 'LAZY', cascade: ['persist'], orphanRemoval: true)]
    #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
    #[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
    /**
     * {@inheritdoc}
     */
    /**
     * {@inheritdoc}
     */
    public function setPasswordRequestedAt(\DateTime $date = null): User
        if ($document->isTwig()) {
            $event->addAction('reload', ['url' => $this->path('admin_invoice_document_reload', ['document' => $document->getId()])]);
        }

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
        if (str_starts_with($uri, '/api/') || stripos($uri, '/register/') !== false) {
        if ($user instanceof User) {
            if (stripos($uri, '/wizard/') !== false) {
                return;
            }
            if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
                return;
            }
            foreach (User::WIZARDS as $wizard) {
                if (!$user->hasSeenWizard($wizard)) {
                    $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard]));
                    $event->setResponse($response);
                    return;
                }
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
                'replaceTags' => true,
                'appendTags' => false,
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
        $container->setParameter('container.dumper.inline_class_loader', true);
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
        $qb->select('COUNT(t)')
            ->orderBy('t.begin', 'DESC');
            $qb->andWhere('t.user = :user');
            $qb->setParameter('user', $user);
use App\Entity\User;
        /** @var User $user */
        $user = $context->getUser();

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
    /**
     * {@inheritdoc}
     */
    /**
     * {@inheritdoc}
     */
    /**
     * {@inheritdoc}
     */
    /**
     * {@inheritdoc}
     */
    /**
     * {@inheritdoc}
     */
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
        return array_merge(['color' => WidgetInterface::COLOR_TOTAL, 'icon' => 'duration'], parent::getOptions($options));
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @param array<string, string|bool|int|null> $options
     * @return array<string, string|bool|int|null>
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
    "symfony/polyfill-ctype": {
        "version": "v1.8.0"
    },
            <th>&nbsp;</th>
                {% block period_name %}{% endblock %}
        {{ 'resetting.check_email'|trans({'%tokenLifetime%': tokenLifetime})|nl2br }}
            <fieldset class="form-fieldset pb-0">
            {{ 'api_password.intro'|trans }}
                {% if not url is empty %}<a href="{{ url }}">{% endif %}
                {% if not url is empty %}</a>{% endif %}
    {{ progress_bar({current: percent, max: 100, min: 0}) }}
    {% if previous is defined %}
        {% if next is defined %}
        $result = json_decode($client->getResponse()->getContent(), true);
        $result = json_decode($client->getResponse()->getContent(), true);
        $result = json_decode($client->getResponse()->getContent(), true);
        $result = json_decode($client->getResponse()->getContent(), true);
        $result = json_decode($client->getResponse()->getContent(), true);
        $result = json_decode($client->getResponse()->getContent(), true);
        $result = json_decode($client->getResponse()->getContent(), true);
        $result = json_decode($client->getResponse()->getContent(), true);
        $result = json_decode($client->getResponse()->getContent(), true);
                'upload_twig' => true,
        self::assertNull($sut->getModifiedAt());
        self::assertNull($sut->getModifiedAt());
        self::assertNull($clone->getModifiedAt());
    public function testConfiguration()
    public function testRender()
    public function testConfiguration()
    public function testRender()
    public function testConfiguration()
    public function testRender()
    public function testSupports()
    public function testRender()
    public function testSupports()
    public function testRender()
        $model->getTemplate()->setLanguage('de');
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
        -
            message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
            count: 9
            path: API/UserControllerTest.php

            message: "#^Parameter \\#6 \\$eventDispatcher of class App\\\\Command\\\\InvoiceCreateCommand constructor expects Symfony\\\\Contracts\\\\EventDispatcher\\\\EventDispatcherInterface, object\\|null given\\.$#"
        -
            message: "#^Cannot call method push\\(\\) on object\\|null\\.$#"
            count: 1
            path: Export/Renderer/PdfRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Export\\\\Renderer\\\\PdfRendererTest\\:\\:testConfiguration\\(\\) has no return type specified\\.$#"
            count: 1
            path: Export/Renderer/PdfRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Export\\\\Renderer\\\\PdfRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
            count: 1
            path: Export/Renderer/PdfRendererTest.php

        -
            message: "#^Parameter \\#2 \\$cacheDirectory of class App\\\\Pdf\\\\MPdfConverter constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#"
            count: 1
            path: Export/Renderer/PdfRendererTest.php

        -
            message: "#^Cannot call method push\\(\\) on object\\|null\\.$#"
            count: 1
            path: Export/Timesheet/HtmlRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Export\\\\Timesheet\\\\HtmlRendererTest\\:\\:testConfiguration\\(\\) has no return type specified\\.$#"
            count: 1
            path: Export/Timesheet/HtmlRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Export\\\\Timesheet\\\\HtmlRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
            count: 1
            path: Export/Timesheet/HtmlRendererTest.php

        -
            message: "#^Cannot call method push\\(\\) on object\\|null\\.$#"
            count: 1
            path: Export/Timesheet/PdfRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Export\\\\Timesheet\\\\PdfRendererTest\\:\\:testConfiguration\\(\\) has no return type specified\\.$#"
            count: 1
            path: Export/Timesheet/PdfRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Export\\\\Timesheet\\\\PdfRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
            count: 1
            path: Export/Timesheet/PdfRendererTest.php

        -
            message: "#^Parameter \\#2 \\$cacheDirectory of class App\\\\Pdf\\\\MPdfConverter constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#"
            count: 1
            path: Export/Timesheet/PdfRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\PdfRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
            count: 1
            path: Invoice/Renderer/PdfRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\PdfRendererTest\\:\\:testSupports\\(\\) has no return type specified\\.$#"
            count: 1
            path: Invoice/Renderer/PdfRendererTest.php

            count: 2
        -
            message: "#^Cannot call method push\\(\\) on object\\|null\\.$#"
            count: 1
            path: Invoice/Renderer/TwigRendererTest.php

        -
            message: "#^Cannot call method setLanguage\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#"
            count: 1
            path: Invoice/Renderer/TwigRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\TwigRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#"
            count: 1
            path: Invoice/Renderer/TwigRendererTest.php

        -
            message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\TwigRendererTest\\:\\:testSupports\\(\\) has no return type specified\\.$#"
            count: 1
            path: Invoice/Renderer/TwigRendererTest.php

      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>استبدل العلامات</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>إلحاق كلمات مفتاحية</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Připojit štítky</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Nahradit štítky</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Erstat etiketter</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Tilføj etiketter</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Schlagworte ersetzen</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Schlagworte hinzufügen</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Schlagworte hinzufügen</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Schlagworte ersetzen</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Αντικατάσταση ετικετών</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Προσάρτηση ετικετών</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Replace tags</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Append tags</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Anstataŭigi etikedojn</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Postglui etikedojn</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Reemplazar etiquetas</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Agregar etiquetas</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Gehitu etiketak</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Ordeztu etiketak</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>جایگزینی برچسب ها</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>پیوست تگ ها</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target state="translated">Vaihda tunnisteet</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target state="translated">Lisää tunnisteita</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Legg aftrat frámerki</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Útskift frámerki</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Ajouter des mots-clés</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Remplacer les mots-clés</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>החלפת תגיות</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>הוספת תגיות</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Zamijeni oznake</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Dodaj oznake</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Címkék megnyitása</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Címkék cseréje</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Sostituisci etichette</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Aggiungi etichette</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>태그 추가</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>태그 교체</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Erstatt etiketter</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Legg til etiketter</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Voeg labels toe</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Vervang labels</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Zamień tagi</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Dodaj tagi</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Anexar etiquetas</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Substituir etiquetas</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Tags de apêndice</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Substituir tags</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Înlocuiește etichetele</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Adaugă etichete</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target state="translated">Добавить метки</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target state="translated">Заменить метки</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Zmeniť značky</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Pridať značky</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Fäst taggar</target>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Ersätt taggar</target>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Etiketleri ekle</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Etiketleri değiştir</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target state="translated">Заміна міток</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target state="translated">Додати мітки</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>Thay thế thẻ</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>Nối các thẻ</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target>替换标签</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target>附加标签</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replaceTags">
        <source>replaceTags</source>
        <target state="translated">替換 tags</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="appendTags">
        <source>appendTags</source>
        <target state="translated">添加 tags</target>
      </trans-unit>
Fixed code: /public/.htaccess
/.env-*
/.idea/
/.DS_Store
/phpstan.sh
/public/.user.ini
/php.ini
/.php-version
/.yarnrc.yml
/.yarn
/config/packages/local.yaml
/var/invoices*
/var/plugins/*/*.disabled
        "kevinpapst/tabler-bundle": "^1.0",
        "symfony/polyfill-ctype": "*",
    "content-hash": "28da6d01423fca5a17395df418603aa5",
            "version": "2.1.4",
                "reference": "72328a11443a0de79967104ad36ba7b30bded134"
                "url": "https://api.github.com/repos/doctrine/collections/zipball/72328a11443a0de79967104ad36ba7b30bded134",
                "reference": "72328a11443a0de79967104ad36ba7b30bded134",
                "doctrine/coding-standard": "^12",
                "source": "https://github.com/doctrine/collections/tree/2.1.4"
            "time": "2023-10-03T09:22:33+00:00"
            "version": "3.7.1",
                "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2"
                "url": "https://api.github.com/repos/doctrine/dbal/zipball/5b7bd66c9ff58c04c5474ab85edce442f8081cb2",
                "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2",
                "phpstan/phpstan": "1.10.35",
                "phpunit/phpunit": "9.6.13",
                "source": "https://github.com/doctrine/dbal/tree/3.7.1"
            "time": "2023-10-06T05:06:20+00:00"
            "version": "1.1.2",
                "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931"
                "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931",
                "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931",
                "source": "https://github.com/doctrine/deprecations/tree/1.1.2"
            "time": "2023-09-27T20:04:15+00:00"
            "version": "4.0.2",
                "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e"
                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e",
                "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e",
                "phpunit/phpunit": "^10.2",
                "vimeo/psalm": "^5.12"
                "source": "https://github.com/egulias/EmailValidator/tree/4.0.2"
            "time": "2023-10-06T06:47:41+00:00"
            "version": "4.8.5",
                "reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77"
                "url": "https://api.github.com/repos/endroid/qr-code/zipball/0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
                "reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
                "source": "https://github.com/endroid/qr-code/tree/4.8.5"
            "time": "2023-09-29T14:03:20+00:00"
            "version": "3.6.0",
                "reference": "e01be8113d4451adb3cbb29d7d2cc96bbc698179"
                "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/e01be8113d4451adb3cbb29d7d2cc96bbc698179",
                "reference": "e01be8113d4451adb3cbb29d7d2cc96bbc698179",
                "symfony/config": "^5.4|^6.0",
                "symfony/dependency-injection": "^5.4|^6.0",
                "symfony/event-dispatcher": "^5.4|^6.0",
                "symfony/http-foundation": "^5.4|^6.0",
                "symfony/http-kernel": "^5.4|^6.0",
                "symfony/routing": "^5.4|^6.0",
                "symfony/security-core": "^5.4|^6.0",
                "sensio/framework-extra-bundle": "<6.1"
                "doctrine/annotations": "^1.13.2|^2.0 ",
                "symfony/asset": "^5.4|^6.0",
                "symfony/browser-kit": "^5.4|^6.0",
                "symfony/css-selector": "^5.4|^6.0",
                "symfony/expression-language": "^5.4|^6.0",
                "symfony/form": "^5.4|^6.0",
                "symfony/mime": "^5.4|^6.0",
                "symfony/phpunit-bridge": "^5.4|^6.0",
                "symfony/security-bundle": "^5.4|^6.0",
                "symfony/serializer": "^5.4|^6.0",
                "symfony/twig-bundle": "^5.4|^6.0",
                "symfony/validator": "^5.4|^6.0",
                "symfony/web-profiler-bundle": "^5.4|^6.0",
                "symfony/yaml": "^5.4|^6.0"
                "source": "https://github.com/FriendsOfSymfony/FOSRestBundle/tree/3.6.0"
            "time": "2023-09-27T11:41:02+00:00"
            "version": "1.0.0",
                "reference": "9554efc6f96d757decbc4b52402eb964e9c90786"
                "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/9554efc6f96d757decbc4b52402eb964e9c90786",
                "reference": "9554efc6f96d757decbc4b52402eb964e9c90786",
                "php": "8.1.*||8.2.*",
                "symfony/asset": "^6.0",
                "symfony/config": "^6.0",
                "symfony/dependency-injection": "^6.0",
                "symfony/event-dispatcher": "^6.0",
                "symfony/http-foundation": "^6.0",
                "symfony/http-kernel": "^6.0",
                "symfony/options-resolver": "^6.0",
                "symfony/security-core": "^6.0",
                "symfony/translation": "^6.0",
                "symfony/twig-bridge": "^6.0",
                "symfony/framework-bundle": "^6.0"
                "source": "https://github.com/kevinpapst/TablerBundle/tree/1.0.0"
            "time": "2023-09-27T13:27:36+00:00"
            "version": "2.13.0",
                "reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba"
                "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/af459883f4018d0f8a0c69c7a209daef3bf973ba",
                "reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba",
                "php": "~8.1.0 || ~8.2.0 || ~8.3.0"
                "infection/infection": "^0.27.0",
                "laminas/laminas-coding-standard": "~2.5.0",
                "phpunit/phpunit": "^9.6.7",
                "psalm/plugin-phpunit": "^0.18.4",
                "vimeo/psalm": "^5.9"
            "time": "2023-10-10T08:35:13+00:00"
            "version": "9.11.0",
                "reference": "33149c4bea4949aa4fa3d03fb11ed28682168b39"
                "url": "https://api.github.com/repos/thephpleague/csv/zipball/33149c4bea4949aa4fa3d03fb11ed28682168b39",
                "reference": "33149c4bea4949aa4fa3d03fb11ed28682168b39",
            "time": "2023-09-23T10:09:54+00:00"
            "version": "v2.0.1",
                "reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
                "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
                "reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
                "source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
            "time": "2023-10-02T14:34:03+00:00"
            "version": "1.24.2",
                "reference": "bcad8d995980440892759db0c32acae7c8e79442"
                "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442",
                "reference": "bcad8d995980440892759db0c32acae7c8e79442",
                "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.2"
            "time": "2023-09-26T12:28:12+00:00"
            "version": "1.0.3",
                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
                "source": "https://github.com/php-fig/http-client"
            "time": "2023-09-23T14:17:50+00:00"
            "version": "v2.5.0",
                "reference": "ecf0459643ec963febfb9a5d529dcd93656006a4"
                "url": "https://api.github.com/repos/Setasign/FPDI/zipball/ecf0459643ec963febfb9a5d529dcd93656006a4",
                "reference": "ecf0459643ec963febfb9a5d529dcd93656006a4",
                "source": "https://github.com/Setasign/FPDI/tree/v2.5.0"
            "time": "2023-09-28T10:46:27+00:00"
            "version": "v6.3.5",
                "reference": "6c1a3ea078c4d88ee892530945df63a87981b2da"
                "url": "https://api.github.com/repos/symfony/cache/zipball/6c1a3ea078c4d88ee892530945df63a87981b2da",
                "reference": "6c1a3ea078c4d88ee892530945df63a87981b2da",
                "source": "https://github.com/symfony/cache/tree/v6.3.5"
            "time": "2023-09-26T15:48:55+00:00"
            "version": "v6.3.5",
                "reference": "2ed62b3bf98346e1f45529a7b6be2196739bb993"
                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2ed62b3bf98346e1f45529a7b6be2196739bb993",
                "reference": "2ed62b3bf98346e1f45529a7b6be2196739bb993",
                "source": "https://github.com/symfony/dependency-injection/tree/v6.3.5"
            "time": "2023-09-25T16:46:40+00:00"
            "version": "v6.3.5",
                "reference": "9977eb1adf999ceded213e88c1ac6dff7a1a0306"
                "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/9977eb1adf999ceded213e88c1ac6dff7a1a0306",
                "reference": "9977eb1adf999ceded213e88c1ac6dff7a1a0306",
                "source": "https://github.com/symfony/doctrine-bridge/tree/v6.3.5"
            "time": "2023-09-29T16:16:03+00:00"
            "version": "v6.3.5",
                "reference": "1f69476b64fb47105c06beef757766c376b548c4"
                "url": "https://api.github.com/repos/symfony/error-handler/zipball/1f69476b64fb47105c06beef757766c376b548c4",
                "reference": "1f69476b64fb47105c06beef757766c376b548c4",
                "source": "https://github.com/symfony/error-handler/tree/v6.3.5"
            "time": "2023-09-12T06:57:20+00:00"
            "version": "v6.3.5",
                "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4"
                "url": "https://api.github.com/repos/symfony/finder/zipball/a1b31d88c0e998168ca7792f222cbecee47428c4",
                "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4",
                "source": "https://github.com/symfony/finder/tree/v6.3.5"
            "time": "2023-09-26T12:56:25+00:00"
            "version": "v6.3.5",
                "reference": "0f9ad8600c1021983d096512066ee54332aa3139"
                "url": "https://api.github.com/repos/symfony/form/zipball/0f9ad8600c1021983d096512066ee54332aa3139",
                "reference": "0f9ad8600c1021983d096512066ee54332aa3139",
                "source": "https://github.com/symfony/form/tree/v6.3.5"
            "time": "2023-09-10T17:47:23+00:00"
            "version": "v6.3.5",
                "reference": "567cafcfc08e3076b47290a7558b0ca17a98b0ce"
                "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/567cafcfc08e3076b47290a7558b0ca17a98b0ce",
                "reference": "567cafcfc08e3076b47290a7558b0ca17a98b0ce",
                "source": "https://github.com/symfony/framework-bundle/tree/v6.3.5"
            "time": "2023-09-29T10:45:15+00:00"
            "version": "v6.3.5",
                "reference": "213e564da4cbf61acc9728d97e666bcdb868c10d"
                "url": "https://api.github.com/repos/symfony/http-client/zipball/213e564da4cbf61acc9728d97e666bcdb868c10d",
                "reference": "213e564da4cbf61acc9728d97e666bcdb868c10d",
                "source": "https://github.com/symfony/http-client/tree/v6.3.5"
            "time": "2023-09-29T15:57:12+00:00"
            "version": "v6.3.5",
                "reference": "b50f5e281d722cb0f4c296f908bacc3e2b721957"
                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b50f5e281d722cb0f4c296f908bacc3e2b721957",
                "reference": "b50f5e281d722cb0f4c296f908bacc3e2b721957",
                "source": "https://github.com/symfony/http-foundation/tree/v6.3.5"
            "time": "2023-09-04T21:33:54+00:00"
            "version": "v6.3.5",
                "reference": "9f991a964368bee8d883e8d57ced4fe9fff04dfc"
                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f991a964368bee8d883e8d57ced4fe9fff04dfc",
                "reference": "9f991a964368bee8d883e8d57ced4fe9fff04dfc",
                "source": "https://github.com/symfony/http-kernel/tree/v6.3.5"
            "time": "2023-09-30T06:37:04+00:00"
            "version": "v6.3.5",
                "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06"
                "url": "https://api.github.com/repos/symfony/mailer/zipball/d89611a7830d51b5e118bca38e390dea92f9ea06",
                "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06",
                "source": "https://github.com/symfony/mailer/tree/v6.3.5"
            "time": "2023-09-06T09:47:15+00:00"
            "version": "v6.3.5",
                "reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e"
                "url": "https://api.github.com/repos/symfony/mime/zipball/d5179eedf1cb2946dbd760475ebf05c251ef6a6e",
                "reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e",
                "source": "https://github.com/symfony/mime/tree/v6.3.5"
            "time": "2023-09-29T06:59:36+00:00"
            "version": "v6.3.5",
                "reference": "278d3a49715073879f75e372ad80b8cfeca949d3"
                "url": "https://api.github.com/repos/symfony/password-hasher/zipball/278d3a49715073879f75e372ad80b8cfeca949d3",
                "reference": "278d3a49715073879f75e372ad80b8cfeca949d3",
                "source": "https://github.com/symfony/password-hasher/tree/v6.3.5"
            "time": "2023-09-25T17:05:16+00:00"
            "version": "v6.3.5",
                "reference": "82616e59acd3e3d9c916bba798326cb7796d7d31"
                "url": "https://api.github.com/repos/symfony/routing/zipball/82616e59acd3e3d9c916bba798326cb7796d7d31",
                "reference": "82616e59acd3e3d9c916bba798326cb7796d7d31",
                "source": "https://github.com/symfony/routing/tree/v6.3.5"
            "time": "2023-09-20T16:05:51+00:00"
            "version": "v6.3.5",
                "reference": "2df460eacceb11b9287cfafddda4d27023dd9001"
                "url": "https://api.github.com/repos/symfony/security-bundle/zipball/2df460eacceb11b9287cfafddda4d27023dd9001",
                "reference": "2df460eacceb11b9287cfafddda4d27023dd9001",
                "symfony/security-http": "^6.3.4"
                "source": "https://github.com/symfony/security-bundle/tree/v6.3.5"
            "time": "2023-09-25T17:05:55+00:00"
            "version": "v6.3.5",
                "reference": "ec8f24dc1195f46483510892271d01a5202bba70"
                "url": "https://api.github.com/repos/symfony/security-core/zipball/ec8f24dc1195f46483510892271d01a5202bba70",
                "reference": "ec8f24dc1195f46483510892271d01a5202bba70",
                "source": "https://github.com/symfony/security-core/tree/v6.3.5"
            "time": "2023-09-10T17:47:23+00:00"
            "version": "v6.3.5",
                "reference": "47058ea557a4c64ba86e9249651222842bd52e2a"
                "url": "https://api.github.com/repos/symfony/security-http/zipball/47058ea557a4c64ba86e9249651222842bd52e2a",
                "reference": "47058ea557a4c64ba86e9249651222842bd52e2a",
                "source": "https://github.com/symfony/security-http/tree/v6.3.5"
            "time": "2023-08-30T06:30:46+00:00"
            "version": "v6.3.5",
                "reference": "855fc058c8bdbb69f53834f2fdb3876c9bc0ab7c"
                "url": "https://api.github.com/repos/symfony/serializer/zipball/855fc058c8bdbb69f53834f2fdb3876c9bc0ab7c",
                "reference": "855fc058c8bdbb69f53834f2fdb3876c9bc0ab7c",
                "source": "https://github.com/symfony/serializer/tree/v6.3.5"
            "time": "2023-09-29T16:18:53+00:00"
            "version": "v6.3.5",
                "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339"
                "url": "https://api.github.com/repos/symfony/string/zipball/13d76d0fb049051ed12a04bef4f9de8715bea339",
                "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339",
                "source": "https://github.com/symfony/string/tree/v6.3.5"
            "time": "2023-09-18T10:38:32+00:00"
            "version": "v6.3.5",
                "reference": "18f2cbe1d46ad43c4d3bd45e5e6279172068e064"
                "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/18f2cbe1d46ad43c4d3bd45e5e6279172068e064",
                "reference": "18f2cbe1d46ad43c4d3bd45e5e6279172068e064",
                "source": "https://github.com/symfony/twig-bridge/tree/v6.3.5"
            "time": "2023-09-12T06:57:20+00:00"
            "version": "v6.3.5",
                "reference": "48e815ba3b5eb72e632588dbf7ea2dc4e608ee47"
                "url": "https://api.github.com/repos/symfony/validator/zipball/48e815ba3b5eb72e632588dbf7ea2dc4e608ee47",
                "reference": "48e815ba3b5eb72e632588dbf7ea2dc4e608ee47",
                "source": "https://github.com/symfony/validator/tree/v6.3.5"
            "time": "2023-09-29T07:41:15+00:00"
            "version": "v6.3.5",
                "reference": "3d9999376be5fea8de47752837a3e1d1c5f69ef5"
                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/3d9999376be5fea8de47752837a3e1d1c5f69ef5",
                "reference": "3d9999376be5fea8de47752837a3e1d1c5f69ef5",
                "source": "https://github.com/symfony/var-dumper/tree/v6.3.5"
            "time": "2023-09-12T10:11:35+00:00"
            "version": "v2.1.0",
                "reference": "50d4af5cca35dab66a51dfeeb63616937ba96f4a"
                "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/50d4af5cca35dab66a51dfeeb63616937ba96f4a",
                "reference": "50d4af5cca35dab66a51dfeeb63616937ba96f4a",
                "symfony/asset": "^5.4 || ^6.2 || ^7.0",
                "symfony/config": "^5.4 || ^6.2 || ^7.0",
                "symfony/dependency-injection": "^5.4 || ^6.2 || ^7.0",
                "symfony/http-kernel": "^5.4 || ^6.2 || ^7.0",
                "symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0",
                "symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0",
                "symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0",
                "symfony/web-link": "^5.4 || ^6.2 || ^7.0"
                "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.1.0"
            "time": "2023-09-26T14:41:29+00:00"
            "version": "4.7.15",
                "reference": "df8de8e484003f68cd2fa68db1e6cfb47a3a92cb"
                "url": "https://api.github.com/repos/zircote/swagger-php/zipball/df8de8e484003f68cd2fa68db1e6cfb47a3a92cb",
                "reference": "df8de8e484003f68cd2fa68db1e6cfb47a3a92cb",
                "source": "https://github.com/zircote/swagger-php/tree/4.7.15"
            "time": "2023-10-12T20:26:34+00:00"
            "version": "3.1.1",
                "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9"
                "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9",
                "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9",
                "source": "https://github.com/composer/pcre/tree/3.1.1"
            "time": "2023-10-11T07:11:09+00:00"
            "version": "v3.35.1",
                "reference": "ec1ccc264994b6764882669973ca435cf05bab08"
                "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/ec1ccc264994b6764882669973ca435cf05bab08",
                "reference": "ec1ccc264994b6764882669973ca435cf05bab08",
                "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.35.1"
            "time": "2023-10-12T13:47:26+00:00"
            "version": "1.10.39",
                "reference": "d9dedb0413f678b4d03cbc2279a48f91592c97c4"
                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d9dedb0413f678b4d03cbc2279a48f91592c97c4",
                "reference": "d9dedb0413f678b4d03cbc2279a48f91592c97c4",
            "time": "2023-10-17T15:46:26+00:00"
            "version": "1.3.15",
                "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a"
                "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/70ecacc64fe8090d8d2a33db5a51fe8e88acd93a",
                "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a",
                "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.15"
            "time": "2023-10-09T18:58:39+00:00"
            "version": "1.3.4",
                "reference": "383855999db6a7d65d0bf580ce2762e17188c2a5"
                "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/383855999db6a7d65d0bf580ce2762e17188c2a5",
                "reference": "383855999db6a7d65d0bf580ce2762e17188c2a5",
                "phpstan/phpstan": "^1.10.36"
                "phpstan/phpstan-phpunit": "^1.3.11",
                "phpstan/phpstan-strict-rules": "^1.5.1",
                "symfony/serializer": "^5.4",
                "symfony/service-contracts": "^2.2.0"
                "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.4"
            "time": "2023-09-29T14:10:11+00:00"
            "version": "9.2.29",
                "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76"
                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76",
                "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76",
                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29"
            "time": "2023-09-19T04:57:46+00:00"
                schema_manager_factory: doctrine.dbal.default_schema_manager_factory
                report_fields_where_declared: true
#   invoice:
#       # all files in these directories will be used as invoice documents (if supported by a renderer)
#       documents:
#           - 'var/invoices/'
#           - 'templates/invoice/renderer/'
#       # allow to upload twig templates as invoice documents
#       upload_twig: true
            login_link:
                check_route: link_login_check
                signature_properties: ['id']
                lifetime: 300
                max_uses: 1

    type: attribute
    type: attribute
    type: attribute
    type: attribute
    type: attribute
    App\Doctrine\ModifiedSubscriber:
        class: App\Doctrine\ModifiedSubscriber
            count: 1
            count: 1
            count: 2
            count: 1
            count: 1
            count: 1
            count: 1
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use App\Repository\Query\BaseQuery;
     * @template TData of BaseQuery
     * @param TData $data
     * @return FormInterface<BaseQuery>
    protected function createSearchForm(string $type, BaseQuery $data, array $options = []): FormInterface
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
    #[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
        $full = $paramFetcher->get('full');
        if ($full === '1' || $full === 'true') {
use Psr\EventDispatcher\EventDispatcherInterface;
    public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'User', 'User_Entity'];
    #[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]

        $full = $paramFetcher->get('full');
        if ($full === '1' || $full === 'true') {
            $view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
        } else {
            $view->getContext()->setGroups(self::GROUPS_COLLECTION);
        }
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Command;

use App\Repository\UserRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;

#[AsCommand(name: 'kimai:user:login-link', description: 'Create a URL that can be used to login as that user', hidden: true)]
/**
 * @CloudRequired
 */
final class UserLoginLinkCommand extends Command
{
    public function __construct(
        private LoginLinkHandlerInterface $loginLink,
        private UserRepository $userRepository,
        private RequestStack $requestStack
    )
    {
        parent::__construct();
        $this->addArgument('email', InputArgument::REQUIRED, 'The email of the user');
        $this->addOption('password-reset', null, InputOption::VALUE_NONE, 'Whether the user needs to reset the password afterwards');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $email = $input->getArgument('email');
        if ($email === null || $email === '') {
            $io->error('Need email to create login URL');

            return Command::FAILURE;
        }

        $user = $this->userRepository->findOneBy(['email' => $email]);

        if ($user === null) {
            $io->error('Need username to create login URL');

            return Command::FAILURE;
        }

        if (!$user->isEnabled()) {
            $io->error('User is not enabled');

            return Command::FAILURE;
        }

        if (!$user->isInternalUser()) {
            $io->error('User does not use internal login');

            return Command::FAILURE;
        }

        $request = new Request();
        $request->setLocale($user->getLocale());
        $this->requestStack->push($request);

        $loginLinkDetails = $this->loginLink->createLoginLink($user, $request);
        $loginLink = $loginLinkDetails->getUrl();

        if ($input->getOption('password-reset') === true) {
            $user->setPasswordRequestedAt(new \DateTime());
            $user->setRequiresPasswordReset(true);
            $this->userRepository->saveUser($user);
        }

        $output->writeln($loginLink);

        return Command::SUCCESS;
    }
}
    public const VERSION = '2.1.0';
    public const VERSION_ID = 20100;
     * The GitHub repository name
     * @template TData of array|object
     * @param TData $data
     * @return FormInterface<TData>
    protected function createFormWithName(string $name, string $type, mixed $data, array $options = []): FormInterface
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
        $content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
        $content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
        $content = $this->renderView('reporting/report_user_list_monthly_export.html.twig', $data);
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
use App\Export\Spreadsheet\Writer\XlsxWriter;
use PhpOffice\PhpSpreadsheet\Reader\Html;
    #[Route(path: '/month_export', name: 'report_user_month_export', methods: ['GET', 'POST'])]
    public function export(Request $request): Response
    {
        $data = $this->getData($request);

        $content = $this->renderView('reporting/report_by_user_data.html.twig', $data);

        $reader = new Html();
        $spreadsheet = $reader->loadFromString($content);

        $writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-monthly');

        return $writer->getFileResponse($spreadsheet);
    }

            'export_route' => 'report_user_month_export',
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
use App\Export\Spreadsheet\Writer\XlsxWriter;
use PhpOffice\PhpSpreadsheet\Reader\Html;
    #[Route(path: '/week_export', name: 'report_user_week_export', methods: ['GET', 'POST'])]
    public function export(Request $request): Response
    {
        $data = $this->getData($request);

        $content = $this->renderView('reporting/report_by_user_data.html.twig', $data);

        $reader = new Html();
        $spreadsheet = $reader->loadFromString($content);

        $writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-weekly');

        return $writer->getFileResponse($spreadsheet);
    }

            'export_route' => 'report_user_week_export',
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
use App\Export\Spreadsheet\Writer\XlsxWriter;
use PhpOffice\PhpSpreadsheet\Reader\Html;
    #[Route(path: '/year_export', name: 'report_user_year_export', methods: ['GET', 'POST'])]
    public function export(Request $request, SystemConfiguration $systemConfiguration): Response
    {
        $data = $this->getData($request, $systemConfiguration);

        $content = $this->renderView('reporting/report_by_user_year_export.html.twig', $data);

        $reader = new Html();
        $spreadsheet = $reader->loadFromString($content);

        $writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-yearly');

        return $writer->getFileResponse($spreadsheet);
    }

            'export_route' => 'report_user_year_export',
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Controller\Security;

use App\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

#[Route(path: '/auth/link')]
/**
 * @CloudRequired
 */
final class LoginLinkController extends AbstractController
{
    #[Route(path: '/check', name: 'link_login_check', methods: ['GET'])]
    public function check(): Response
    {
        return new Response();
    }
}
use Psr\EventDispatcher\EventDispatcherInterface;
            'tokenLifetime' => $this->configuration->getPasswordResetRetryLifetime(),
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
                if ($dto->isReplaceDescription()) {
                    $timesheet->setDescription($dto->getDescription());
                    $execute = true;
                } elseif($dto->getDescription() !== null && $dto->getDescription() !== '') {
                    $timesheet->setDescription($timesheet->getDescription() . PHP_EOL . $dto->getDescription());
                    $execute = true;
                }
use Psr\EventDispatcher\EventDispatcherInterface;
use App\Form\UserPasswordType;
            $next = 'done';
            if ($user->requiresPasswordReset()) {
                $next = 'password';
            }

                    return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $data['language']]);
                'next' => $next,
                'form' => $form->createView(),
            ]);
        }

        if ($wizard === 'password' || $user->requiresPasswordReset()) {
            $form = $this->createForm(UserPasswordType::class, $user, [
                'action' => $this->generateUrl('wizard', ['wizard' => 'password']),
                'method' => 'POST',
            ]);

            $form->handleRequest($request);

            if ($form->isSubmitted() && $form->isValid()) {
                $user->setRequiresPasswordReset(false);
                $userService->updateUser($user);

                return $this->redirectToRoute('wizard', ['wizard' => 'done']);
            }

            $previous = 'profile';
            $percent = \intval(100 / \count(User::WIZARDS) * 1);

            if ($user->requiresPasswordReset()) {
                $previous = null;
                $percent = null;
            }

            return $this->render('wizard/password.html.twig', [
                'percent' => $percent,
                'previous' => $previous,
use Psr\EventDispatcher\EventDispatcherInterface;
                    ->defaultFalse()
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Doctrine;

use App\Entity\Timesheet;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;

/**
 * Automatically set the modifiedAt field for all Timesheet entries.
 */
#[AsDoctrineListener(event: Events::onFlush, priority: 60)]
final class ModifiedSubscriber implements EventSubscriber, DataSubscriberInterface
{
    public function getSubscribedEvents(): array
    {
        return [
            Events::onFlush,
        ];
    }

    public function onFlush(OnFlushEventArgs $args): void
    {
        $uow = $args->getObjectManager()->getUnitOfWork();
        $now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));

        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            if (!($entity instanceof Timesheet)) {
                continue;
            }
            $entity->setModifiedAt($now);
        }

        foreach ($uow->getScheduledEntityInsertions() as $entity) {
            if (!($entity instanceof Timesheet)) {
                continue;
            }
            $entity->setModifiedAt($now);
        }
    }
}
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
#[AsDoctrineListener(event: Events::onFlush, priority: 50)]
    #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
    #[ORM\OneToMany(mappedBy: 'activity', targetEntity: ActivityMeta::class, cascade: ['persist'])]
    #[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'activities', cascade: ['persist'])]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\OneToMany(mappedBy: 'customer', targetEntity: CustomerMeta::class, cascade: ['persist'])]
    #[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'customers', cascade: ['persist'])]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\OneToMany(mappedBy: 'invoice', targetEntity: InvoiceMeta::class, cascade: ['persist'])]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectMeta::class, cascade: ['persist'])]
    #[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'projects', cascade: ['persist'])]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[Assert\Regex(pattern: '/,/', message: 'Tag name cannot contain comma', match: false)]
    #[ORM\OneToMany(mappedBy: 'team', targetEntity: TeamMember::class, cascade: ['persist', 'remove'], fetch: 'LAZY', orphanRemoval: true)]
    #[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
    #[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
    #[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private \DateTimeInterface $modifiedAt;
    #[ORM\OneToMany(mappedBy: 'timesheet', targetEntity: TimesheetMeta::class, cascade: ['persist'])]
        $this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
    public function getModifiedAt(): \DateTimeInterface
    public function setModifiedAt(\DateTimeInterface $dateTime): void
    {
        $this->modifiedAt = $dateTime;
    }

        $this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Exporter\Expose(name: 'last_login', label: 'lastLogin', type: 'datetime', exp: 'object.getLastLogin()')]
#[Exporter\Expose(name: 'roles', label: 'roles', type: 'array', exp: 'object.getRoles()')]
#[Exporter\Expose(name: 'active', label: 'active', type: 'boolean', exp: 'object.isEnabled()')]
    #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserPreference::class, cascade: ['persist'])]
    #[ORM\OneToMany(mappedBy: 'user', targetEntity: TeamMember::class, cascade: ['persist'], fetch: 'LAZY', orphanRemoval: true)]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    #[Serializer\VirtualProperty]
    #[Serializer\SerializedName('apiToken')]
    #[Serializer\Groups(['Default'])]
    public function hasApiToken(): bool
    {
        return $this->apiToken !== null;
    }

    public function setPasswordRequestedAt(?\DateTime $date = null): User
    public function requiresPasswordReset(): bool
    {
        if (!$this->isInternalUser() || !$this->isEnabled()) {
            return false;
        }

        return $this->getPreferenceValue('__pw_reset__') === '1';
    }

    public function setRequiresPasswordReset(bool $require = true): void
    {
        $this->setPreferenceValue('__pw_reset__', ($require ? '1' : '0'));
    }

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
        if (str_starts_with($uri, '/api/') || stripos($uri, '/register/') !== false || stripos($uri, '/wizard/') !== false) {
        if (!($user instanceof User)) {
            return;
        }
        if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
            return;
        }
        foreach (User::WIZARDS as $wizard) {
            if (!$user->hasSeenWizard($wizard)) {
                $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard]));
                $event->setResponse($response);
                return;

        if ($user->requiresPasswordReset()) {
            $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => 'password']));
            $event->setResponse($response);
        }
use Psr\EventDispatcher\EventDispatcherInterface;
use App\Twig\SecurityPolicy\ExportPolicy;
use Psr\EventDispatcher\EventDispatcherInterface;
use Twig\Extension\SandboxExtension;
        // enable basic security measures
        $sandbox = new SandboxExtension(new ExportPolicy());
        $sandbox->enableSandbox();
        $this->twig->addExtension($sandbox);

use App\Twig\SecurityPolicy\ExportPolicy;
use Twig\Extension\SandboxExtension;

        // enable basic security measures
        $sandbox = new SandboxExtension(new ExportPolicy());
        $sandbox->enableSandbox();
        $this->twig->addExtension($sandbox);

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use App\Form\Type\DescriptionType;
            'label_attr' => [
                'class' => 'radio-inline',
            ],
                'append' => false,
                'replace' => true,
        $builder->add('replaceDescription', ChoiceType::class, [
            'label' => false,
            'required' => true,
            'expanded' => true,
            'label_attr' => [
                'class' => 'radio-inline',
            ],
            'choices' => [
                'append' => false,
                'replace' => true,
            ]
        ]);

        $builder->add('description', DescriptionType::class, [
            'required' => false,
        ]);

    private bool $replaceDescription = false;
    private ?string $description = null;
    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): void
    {
        $this->description = $description;
    }

    public function isReplaceDescription(): bool
    {
        return $this->replaceDescription;
    }

    public function setReplaceDescription(bool $replaceDescription): void
    {
        $this->replaceDescription = $replaceDescription;
    }

use Psr\EventDispatcher\EventDispatcherInterface;
            'label' => 'skin',
use App\Form\Type\YesNoType;

        $builder->add('requiresPasswordReset', YesNoType::class, [
            'label' => 'force_password_change',
            'help' => 'force_password_change_help',
            'required' => false,
        ]);
use Psr\EventDispatcher\EventDispatcherInterface;
        $container->setParameter('.container.dumper.inline_class_loader', true);
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
        $qb->select($qb->expr()->count('t'))
        ;
            $qb
                ->andWhere('t.user = :user')
                ->groupBy('t.user')
                ->setParameter('user', $user)
            ;
use Psr\EventDispatcher\EventDispatcherInterface;
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Twig\SecurityPolicy;

use Twig\Sandbox\SecurityPolicyInterface;

final class ChainPolicy implements SecurityPolicyInterface
{
    /** @var array<SecurityPolicyInterface> */
    private array $policies = [];

    public function __construct()
    {
    }

    public function addPolicy(SecurityPolicyInterface $policy): void
    {
        $this->policies[] = $policy;
    }

    public function checkSecurity($tags, $filters, $functions): void
    {
        foreach ($this->policies as $policy) {
            $policy->checkSecurity($tags, $filters, $functions);
        }
    }

    public function checkMethodAllowed($obj, $method): void
    {
        foreach ($this->policies as $policy) {
            $policy->checkMethodAllowed($obj, $method);
        }
    }

    public function checkPropertyAllowed($obj, $property): void
    {
        foreach ($this->policies as $policy) {
            $policy->checkPropertyAllowed($obj, $property);
        }
    }
}
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Twig\SecurityPolicy;

use Twig\Sandbox\SecurityPolicyInterface;

/**
 * The Twig environment needs the sandbox extension, which itself needs a policy to start working.
 */
final class DefaultPolicy implements SecurityPolicyInterface
{
    public function checkSecurity($tags, $filters, $functions): void
    {
    }

    public function checkMethodAllowed($obj, $method): void
    {
    }

    public function checkPropertyAllowed($obj, $property): void
    {
    }
}
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Twig\SecurityPolicy;

use Twig\Sandbox\SecurityPolicyInterface;

/**
 * Represents the security policy for custom Twig export templates.
 */
final class ExportPolicy implements SecurityPolicyInterface
{
    private ChainPolicy $policy;

    public function __construct()
    {
        $this->policy = new ChainPolicy();
        $this->policy->addPolicy(new DefaultPolicy());
    }

    public function checkSecurity($tags, $filters, $functions): void
    {
        $this->policy->checkSecurity($tags, $filters, $functions);
    }

    public function checkMethodAllowed($obj, $method): void
    {
        $this->policy->checkMethodAllowed($obj, $method);
    }

    public function checkPropertyAllowed($obj, $property): void
    {
        $this->policy->checkPropertyAllowed($obj, $property);
    }
}
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Twig\SecurityPolicy;

use Twig\Markup;
use Twig\Sandbox\SecurityNotAllowedFilterError;
use Twig\Sandbox\SecurityNotAllowedFunctionError;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Template;

/**
 * A blocking approach for Twig templates.
 */
final class ForbiddenPolicy implements SecurityPolicyInterface
{
    /** @var array<string, array<string>> */
    private array $forbiddenMethods = [];

    /**
     * @param array<string> $forbiddenTags
     * @param array<string> $forbiddenFilters
     * @param array<string, array<string>> $forbiddenMethods
     * @param array<string, array<string>> $forbiddenProperties
     * @param array<string> $forbiddenFunctions
     */
    public function __construct(
        private array $forbiddenTags = [],
        private array $forbiddenFilters = [],
        array $forbiddenMethods = [],
        private array $forbiddenProperties = [],
        private array $forbiddenFunctions = []
    )
    {
        $this->forbiddenMethods = [];
        foreach ($forbiddenMethods as $class => $m) {
            $this->forbiddenMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]);
        }
    }

    public function checkSecurity($tags, $filters, $functions): void
    {
        foreach ($tags as $tag) {
            if (\in_array($tag, $this->forbiddenTags)) {
                throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag);
            }
        }

        foreach ($filters as $filter) {
            if (\in_array($filter, $this->forbiddenFilters)) {
                throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter);
            }
        }

        foreach ($functions as $function) {
            if (\in_array($function, $this->forbiddenFunctions)) {
                throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function);
            }
        }
    }

    public function checkMethodAllowed($obj, $method): void
    {
        if ($obj instanceof Template || $obj instanceof Markup) {
            return;
        }

        $forbidden = false;
        $method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
        foreach ($this->forbiddenMethods as $class => $methods) {
            if ($obj instanceof $class) {
                $forbidden = \in_array($method, $methods);

                break;
            }
        }

        if ($forbidden) {
            $class = \get_class($obj);
            throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
        }
    }

    public function checkPropertyAllowed($obj, $property): void
    {
        $forbidden = false;
        foreach ($this->forbiddenProperties as $class => $properties) {
            if ($obj instanceof $class) {
                $forbidden = \in_array($property, \is_array($properties) ? $properties : [$properties]);

                break;
            }
        }

        if ($forbidden) {
            $class = \get_class($obj);
            throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
        }
    }
}
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Twig\SecurityPolicy;

use App\Invoice\InvoiceModel;
use App\Pdf\PdfContext;
use Symfony\Component\String\UnicodeString;
use Twig\Markup;
use Twig\Sandbox\SecurityPolicy;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Template;

/**
 * Represents the security policy for custom Twig invoice templates.
 */
final class InvoicePolicy implements SecurityPolicyInterface
{
    private ChainPolicy $policy;

    public function __construct()
    {
        $this->policy = new ChainPolicy();
        $this->policy->addPolicy(new DefaultPolicy());
        $this->policy->addPolicy(new SecurityPolicy(
            ['block', 'if', 'for', 'set', 'extends'],
            [
                // Twig core filters
                'map', 'escape', 'trans', 'default', 'nl2br', 'trim', 'raw',
                'join', 'u', 'slice', 'date', 'month_name', 'first', 'country_name',
                'replace', 'length', 'number_format', 'split',

                // Kimai filters
                'md2html', 'desc2html', 'comment2html', 'comment1line', 'multiline_indent', 'nl2str',
                'date_short', 'duration', 'amount', 'money', 'duration_decimal',
            ],
            [
                PdfContext::class => ['setoption'],
                InvoiceModel::class => ['toarray'],
            ],
            [], // properties
            [
                // Twig core functions
                'cycle', 'asset', 'range',

                // Kimai functions
                'encore_entry_css_source', 'qr_code_data_uri', 'config',
            ]
        ));
    }

    public function checkSecurity($tags, $filters, $functions): void
    {
        $this->policy->checkSecurity($tags, $filters, $functions);
    }

    public function checkMethodAllowed($obj, $method): void
    {
        if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) {
            return;
        }

        $lm = strtolower($method);

        if (str_starts_with($lm, 'get') || str_starts_with($lm, 'is') || str_starts_with($lm, 'has')) {
            return;
        }

        if ($lm === '__tostring') {
            return;
        }

        $this->policy->checkMethodAllowed($obj, $method);
    }

    public function checkPropertyAllowed($obj, $property): void
    {
        $this->policy->checkPropertyAllowed($obj, $property);
    }
}
use App\Twig\SecurityPolicy\InvoicePolicy;
use Twig\Extension\SandboxExtension;
        // enable basic security measures
        if (!$twig->hasExtension(SandboxExtension::class)) {
            $sandbox = new SandboxExtension(new InvoicePolicy());
            $sandbox->enableSandbox();
            $twig->addExtension($sandbox);
        }

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
use Psr\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
use Psr\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @return array<string, string|bool|int|null|array<string, mixed>>
use App\Repository\Query\TimesheetQuery;
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @return array<string, string|bool|int|null|array<string, mixed>>
        // we can safely assume that the user can see
        $route = 'admin_timesheet';

        return array_merge([
            'color' => WidgetInterface::COLOR_TOTAL,
            'icon' => 'duration',
            'route' => $route,
            'routeOptions' => ['state' => TimesheetQuery::STATE_RUNNING],
        ], parent::getOptions($options));
        // if you ever loosen that check, make sure that the above link is probably removed
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
use Psr\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
use Psr\EventDispatcher\EventDispatcherInterface;
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     @return array<string, string|bool|int|null|array<string, mixed>>
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @param array<string, string|bool|int|null|array<string, mixed>> $options
     * @return array<string, string|bool|int|null|array<string, mixed>>
use Psr\EventDispatcher\EventDispatcherInterface;
        {% block upper_left %}&nbsp;{% endblock %}
            <th>{% block upper_left %}{{ user.displayName }}{% endblock %}</th>
                {% block period_name %}
                    <th>{{ column|date_short }}</th>
                {% endblock %}
    {% from '@theme/components/buttons.html.twig' import submit_button %}
    {{ submit_button('download', {'attr': {'formaction': path(export_route)}, 'icon': 'download', 'combined': false}, 'primary') }}
        {% block upper_left %}&nbsp;{% endblock %}
{% embed 'reporting/report_by_user_data.html.twig' %}
    {% block period_name %}
        <th>{{ column|month_name }} {{ column|date_format('Y') }}</th>
    {% endblock %}
    {% block column_classes_project %}{% endblock %}
    {% block column_classes_activity %}{% endblock %}
    {% block column_classes_total %}{% endblock %}
{% endembed %}
        {{ 'resetting.check_email'|trans({'%tokenLifetime%': tokenLifetime|duration})|nl2br }}
            <fieldset class="form-fieldset pb-0 h-100">
                {{ form_row(form.replaceDescription) }}
                {{ form_row(form.description) }}
            </fieldset>
            <fieldset class="form-fieldset pb-0 h-100">
            <div class="me-auto">
                <p>{{ 'api_password.intro'|trans }}</p>

                <p>{{ 'username'|trans }}: {{ user.userIdentifier }}</p>
            </div>
                {% if not url is empty %}<a href="{{ url }}">{% endif %}
                {% if not url is empty %}</a>{% endif %}

    {% if percent is defined and percent is not null %}
        {{ progress_bar({current: percent, max: 100, min: 0}) }}
    {% endif %}
    {% if previous is defined and previous is not null %}
        {% if next is defined and next is not null %}
{% extends 'wizard/layout.html.twig' %}

{% block wizard_content %}
    <div class="card-body text-center py-4 p-sm-6">
        <h1 class="mt-2">{{ 'wizard.password.title'|trans({}, 'wizard') }}</h1>
        <p class="text-body-secondary">{{ 'wizard.password.description'|trans({}, 'wizard') }}</p>
    </div>
    <div class="hr-text hr-text-center hr-text-spaceless">{{ 'wizard.separator'|trans({}, 'wizard') }}</div>
    <div class="card-body">
        <div class="mb-3">
            {{ form_rest(form) }}
        </div>
    </div>
{% endblock %}
                    'apiToken' => 'bool',
                    'apiToken' => 'bool',
        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);
    public function testGetCollectionFull(): void
    {
        $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
        $this->assertAccessIsGranted($client, '/api/users?full=true');

        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);

        $this->assertIsArray($result);
        $this->assertNotEmpty($result);
        $this->assertEquals(7, \count($result));
        foreach ($result as $user) {
            self::assertApiResponseTypeStructure('UserEntity', $user);
        }
    }

        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);
        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);
        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);
        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);
        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);
        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);
        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);
        $content = $client->getResponse()->getContent();
        $this->assertIsString($content);
        $result = json_decode($content, true);
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Tests\Command;

use App\Command\UserLoginLinkCommand;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;

/**
 * @covers \App\Command\UserLoginLinkCommand
 * @group integration
 */
class UserLoginLinkCommandTest extends KernelTestCase
{
    private Application $application;

    protected function setUp(): void
    {
        parent::setUp();
        $kernel = self::bootKernel();
        $this->application = new Application($kernel);

        $loginLinkHandler = $this->createMock(LoginLinkHandlerInterface::class);
        $userRepository = $this->createMock(UserRepository::class);
        $requestStack = $this->createMock(RequestStack::class);

        $this->application->add(new UserLoginLinkCommand($loginLinkHandler, $userRepository, $requestStack));
    }

    public function testCommandName(): void
    {
        $application = $this->application;

        $command = $application->find('kimai:user:login-link');
        self::assertInstanceOf(UserLoginLinkCommand::class, $command);
    }
}
                'upload_twig' => false,
        self::assertNotNull($sut->getModifiedAt());
        self::assertNotNull($sut->getModifiedAt());
        self::assertNotNull($clone->getModifiedAt());
use Symfony\Component\HttpFoundation\RequestStack;
    public function testConfiguration(): void
    public function testRenderAttachment(): void
        /** @var RequestStack $stack */
        /** @var string $cacheDir */
    }

    public function testRenderInline(): void
    {
        $kernel = self::bootKernel();
        /** @var Environment $twig */
        $twig = self::getContainer()->get('twig');
        /** @var RequestStack $stack */
        $stack = self::getContainer()->get('request_stack');
        /** @var string $cacheDir */
        $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
        $converter = new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir);
        $request = new Request();
        $request->setLocale('en');
        $stack->push($request);

        $sut = new PDFRenderer($twig, $converter, $this->createMock(ProjectStatisticService::class));

        $prefix = date('Ymd');
        $this->assertEquals('application/pdf', $response->headers->get('Content-Type'));
        $this->assertNotEmpty($response->getContent());
use Symfony\Component\HttpFoundation\RequestStack;
    public function testConfiguration(): void
    public function testRender(): void
        /** @var RequestStack $stack */
use Symfony\Component\HttpFoundation\RequestStack;
    public function testConfiguration(): void
    public function testRender(): void
        /** @var RequestStack $stack */
        /** @var string $cacheDir */
use App\Model\InvoiceDocument;
use Symfony\Component\HttpFoundation\RequestStack;
    public function testSupports(): void
    public function testRenderAttachment(): void
        /** @var RequestStack $stack */
    }

    public function testRenderInline(): void
    {
        $kernel = self::bootKernel();
        /** @var Environment $twig */
        $twig = self::getContainer()->get('twig');
        /** @var RequestStack $stack */
        $stack = self::getContainer()->get('request_stack');
        /** @var string $cacheDir */
        $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');

        $request = new Request();
        $request->setLocale('en');
        $stack->push($request);

        /** @var FilesystemLoader $loader */
        $loader = $twig->getLoader();
        $loader->addPath(__DIR__ . '/../templates/', 'invoice');

        $sut = new PdfRenderer($twig, new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir));
        $model = $this->getInvoiceModel();
        $document = $this->getInvoiceDocument('default.pdf.twig', true);
        $this->assertNotEmpty($response->getContent());
    }

    public function testRenderAll(): void
    {
        $kernel = self::bootKernel();
        /** @var Environment $twig */
        $twig = self::getContainer()->get('twig');
        $stack = self::getContainer()->get('request_stack');
        /** @var string $cacheDir */
        $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');

        $request = new Request();
        $request->setLocale('en');
        $stack->push($request);

        /** @var FilesystemLoader $loader */
        $loader = $twig->getLoader();
        $loader->addPath(__DIR__ . '/../templates/', 'invoice');

        $dirs = [
            __DIR__ . '/../../../templates/invoice/renderer/',
            __DIR__ . '/../../../var/invoices/',
            __DIR__ . '/../../../var/invoices_customer/',
            __DIR__ . '/../../../var/invoices_old/',
        ];

        $files = [];
        foreach ($dirs as $dir) {
            if (!is_dir($dir)) {
                continue;
            }
            $dir = realpath($dir);
            $loader->addPath($dir . '/', 'invoice');
            $found = glob($dir . '/*.pdf.twig');
            if ($found !== false) {
                $files = array_merge($files, $found);
            }
        }

        $sut = new PdfRenderer($twig, new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir));
        $model = $this->getInvoiceModel();

        foreach ($files as $filename) {
            $document = new InvoiceDocument(new \SplFileInfo($filename));

            $response = $sut->render($document, $model);
            $this->assertEquals('application/pdf', $response->headers->get('Content-Type'));
            $this->assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition'));
            $this->assertNotEmpty($response->getContent());
        }
use App\Model\InvoiceDocument;
use Symfony\Component\HttpFoundation\RequestStack;
    public function testSupports(): void
    public function testRender(): void
        /** @var RequestStack $stack */
        $model->getTemplate()?->setLanguage('de');

    public function testRenderAll(): void
    {
        $kernel = self::bootKernel();
        /** @var Environment $twig */
        $twig = self::getContainer()->get('twig');
        /** @var RequestStack $stack */
        $stack = self::getContainer()->get('request_stack');
        $request = new Request();
        $request->setLocale('en');
        $stack->push($request);

        /** @var FilesystemLoader $loader */
        $loader = $twig->getLoader();
        $loader->addPath($this->getInvoiceTemplatePath(), 'invoice');

        $dirs = [
            __DIR__ . '/../../../templates/invoice/renderer/',
            __DIR__ . '/../../../var/invoices/',
            __DIR__ . '/../../../var/invoices_customer/',
            __DIR__ . '/../../../var/invoices_old/',
        ];

        $files = [];
        foreach ($dirs as $dir) {
            if (!is_dir($dir)) {
                continue;
            }
            $dir = realpath($dir);
            $loader->addPath($dir . '/', 'invoice');
            $found = glob($dir . '/*.html.twig');
            if ($found !== false) {
                $files = array_merge($files, $found);
            }
        }

        $sut = new TwigRenderer($twig);

        $model = $this->getInvoiceModel();
        $model->getTemplate()?->setLanguage('de');

        foreach ($files as $filename) {
            $document = new InvoiceDocument(new \SplFileInfo($filename));

            $response = $sut->render($document, $model);
            $this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-Type'));
            $this->assertNotEmpty($response->getContent());
        }
    }
{% set language = invoice['invoice.language'] %}
            message: "#^Parameter \\#6 \\$eventDispatcher of class App\\\\Command\\\\InvoiceCreateCommand constructor expects Psr\\\\EventDispatcher\\\\EventDispatcherInterface, object\\|null given\\.$#"
            count: 3
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Připojit</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Nahradit</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Erstat</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Tilføj</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Ersetzen</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Hinzufügen</target>
      <trans-unit id=".wmq_rT" resname="force_password_change">
        <source>force_password_change</source>
        <target>Neues Passwort anfordern</target>
      </trans-unit>
      <trans-unit id="0bKwA0k" resname="force_password_change_help">
        <source>force_password_change_help</source>
        <target>Bei der nächsten Anmeldung muss der Benutzer ein neues Passwort hinterlegen</target>
      </trans-unit>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Hinzufügen</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Ersetzen</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Αντικατάσταση</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Προσάρτηση</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Replace</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Append</target>
      <trans-unit id=".wmq_rT" resname="force_password_change">
        <source>force_password_change</source>
        <target>Request new password</target>
      </trans-unit>
      <trans-unit id="0bKwA0k" resname="force_password_change_help">
        <source>force_password_change_help</source>
        <target>The next time the user logs in, they must enter a new password.</target>
      </trans-unit>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Anstataŭigi</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Postglui</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Reemplazar</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Gehitu</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Ordeztu</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target state="translated">Vaihda</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target state="translated">Lisää</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Legg aftrat</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Útskift</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Ajouter</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Remplacer</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Zamijeni</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Dodaj</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Sostituisci</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Aggiungi</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Zamień</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Dodaj</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Anexar</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Substituir</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target state="translated">Добавить</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target state="translated">Заменить</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Zmeniť</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Pridať</target>
      <trans-unit id="ICpl9sx" resname="append">
        <source>append</source>
        <target>Fäst</target>
      <trans-unit id="tl01EdS" resname="replace">
        <source>replace</source>
        <target>Ersätt</target>
      <trans-unit id="7Vipfs7" resname="wizard.password.title">
        <source>wizard.password.title</source>
        <target>Ihr Passwort</target>
      </trans-unit>
      <trans-unit id="zlNbbUg" resname="wizard.password.description">
        <source>wizard.password.description</source>
        <target>Bitte geben Sie ihr neues Passwort ein, das von nun an für die Anmeldung gelten soll.</target>
      </trans-unit>
      <trans-unit id="7Vipfs7" resname="wizard.password.title">
        <source>wizard.password.title</source>
        <target>Your password</target>
      </trans-unit>
      <trans-unit id="zlNbbUg" resname="wizard.password.description">
        <source>wizard.password.description</source>
        <target>Please enter your new password, which will be used for logging in from now on.</target>
      </trans-unit>