CVE-2023-46245 (published 2023-10-31T16:15Z)

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.

CVSS v3 base score: 7.2, HIGH (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-Other
cwe_name: Other
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.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/kimai/kimai
commit_details: repo_url: https://github.com/kimai/kimai
author: Kevin Papst
author_date: 2023-10-19 11:21:50+02:00
msg: 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
repository: repo_url: https://github.com/kimai/kimai
repo_name: kimai/kimai
description: Kimai is a web-based multi-user time-tracking application. Works great for everyone: freelancers, companies, organizations - everyone can track their times, generate reports, create invoices and do so much more. SaaS version available at https://www.kimai.cloud
repo_language: PHP
owner: kimai
file_changes: filename: .gitignore
change_type: ModificationType.MODIFY
diff: @@ -1,45 +1,34 @@
 # 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

programming_language: PHP
filename: composer.json
change_type: ModificationType.MODIFY
diff: @@ -35,7 +35,7 @@
         "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",
@@ -129,6 +129,7 @@
         }
     },
     "replace": {
+        "symfony/polyfill-ctype": "*",
         "symfony/polyfill-mbstring": "*",
         "symfony/polyfill-intl": "*",
         "symfony/polyfill-iconv": "*",

programming_language: Jupyter Notebook
filename: composer.lock
change_type: ModificationType.MODIFY
diff: @@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "9159cc3cf1d749c412d1353e921de6fc",
+    "content-hash": "28da6d01423fca5a17395df418603aa5",
     "packages": [
         {
             "name": "azuyalabs/yasumi",
@@ -485,16 +485,16 @@
         },
         {
             "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": {
@@ -502,7 +502,7 @@
                 "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",
@@ -551,7 +551,7 @@
             ],
             "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": [
                 {
@@ -567,7 +567,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-06T15:15:36+00:00"
+            "time": "2023-10-03T09:22:33+00:00"
         },
         {
             "name": "doctrine/common",
@@ -662,16 +662,16 @@
         },
         {
             "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": {
@@ -687,9 +687,9 @@
                 "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",
@@ -755,7 +755,7 @@
             ],
             "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": [
                 {
@@ -771,20 +771,20 @@
                     "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": {
@@ -816,9 +816,9 @@
             "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",
@@ -1706,16 +1706,16 @@
         },
         {
             "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": {
@@ -1724,8 +1724,8 @@
                 "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"
@@ -1761,7 +1761,7 @@
             ],
             "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": [
                 {
@@ -1769,20 +1769,20 @@
                     "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": {
@@ -1836,7 +1836,7 @@
             ],
             "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": [
                 {
@@ -1844,7 +1844,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-08-28T18:12:07+00:00"
+            "time": "2023-09-29T14:03:20+00:00"
         },
         {
             "name": "erusev/parsedown",
@@ -1959,28 +1959,28 @@
         },
         {
             "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"
             },
@@ -1988,30 +1988,29 @@
                 "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",
@@ -2059,9 +2058,9 @@
             ],
             "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",
@@ -2439,41 +2438,40 @@
         },
         {
             "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"
@@ -2497,7 +2495,7 @@
             "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": [
                 {
@@ -2509,37 +2507,37 @@
                     "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": {
@@ -2571,20 +2569,20 @@
                     "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": {
@@ -2659,7 +2657,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-08-04T15:12:48+00:00"
+            "time": "2023-09-23T10:09:54+00:00"
         },
         {
             "name": "lorenzo/pinky",
@@ -3086,16 +3084,16 @@
         },
         {
             "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": {
@@ -3128,9 +3126,9 @@
             "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",
@@ -4068,16 +4066,16 @@
         },
         {
             "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": {
@@ -4109,9 +4107,9 @@
             "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",
@@ -4315,16 +4313,16 @@
         },
         {
             "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": {
@@ -4361,9 +4359,9 @@
                 "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",
@@ -4785,16 +4783,16 @@
         },
         {
             "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": {
@@ -4845,7 +4843,7 @@
             ],
             "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": [
                 {
@@ -4853,7 +4851,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-27T08:12:09+00:00"
+            "time": "2023-09-28T10:46:27+00:00"
         },
         {
             "name": "spomky-labs/otphp",
@@ -5006,16 +5004,16 @@
         },
         {
             "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": {
@@ -5082,7 +5080,7 @@
                 "psr6"
             ],
             "support": {
-                "source": "https://github.com/symfony/cache/tree/v6.3.4"
+                "source": "https://github.com/symfony/cache/tree/v6.3.5"
             },
             "funding": [
                 {
@@ -5098,7 +5096,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-05T09:10:27+00:00"
+            "time": "2023-09-26T15:48:55+00:00"
         },
         {
             "name": "symfony/cache-contracts",
@@ -5481,16 +5479,16 @@
         },
         {
             "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": {
@@ -5542,7 +5540,7 @@
             "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": [
                 {
@@ -5558,7 +5556,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-16T17:55:17+00:00"
+            "time": "2023-09-25T16:46:40+00:00"
         },
         {
             "name": "symfony/deprecation-contracts",
@@ -5629,16 +5627,16 @@
         },
         {
             "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": {
@@ -5719,7 +5717,7 @@
             "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": [
                 {
@@ -5735,7 +5733,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-08T10:40:25+00:00"
+            "time": "2023-09-29T16:16:03+00:00"
         },
         {
             "name": "symfony/dotenv",
@@ -5813,16 +5811,16 @@
         },
         {
             "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": {
@@ -5867,7 +5865,7 @@
             "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": [
                 {
@@ -5883,7 +5881,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-16T17:05:46+00:00"
+            "time": "2023-09-12T06:57:20+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
@@ -6170,16 +6168,16 @@
         },
         {
             "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": {
@@ -6214,7 +6212,7 @@
             "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": [
                 {
@@ -6230,7 +6228,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-31T08:31:44+00:00"
+            "time": "2023-09-26T12:56:25+00:00"
         },
         {
             "name": "symfony/flex",
@@ -6299,16 +6297,16 @@
         },
         {
             "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": {
@@ -6376,7 +6374,7 @@
             "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": [
                 {
@@ -6392,20 +6390,20 @@
                     "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": {
@@ -6520,7 +6518,7 @@
             "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": [
                 {
@@ -6536,20 +6534,20 @@
                     "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": {
@@ -6612,7 +6610,7 @@
                 "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": [
                 {
@@ -6628,7 +6626,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-05T08:41:27+00:00"
+            "time": "2023-09-29T15:57:12+00:00"
         },
         {
             "name": "symfony/http-client-contracts",
@@ -6710,16 +6708,16 @@
         },
         {
             "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": {
@@ -6767,7 +6765,7 @@
             "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": [
                 {
@@ -6783,20 +6781,20 @@
                     "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": {
@@ -6880,7 +6878,7 @@
             "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": [
                 {
@@ -6896,7 +6894,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-26T13:54:49+00:00"
+            "time": "2023-09-30T06:37:04+00:00"
         },
         {
             "name": "symfony/intl",
@@ -6982,16 +6980,16 @@
         },
         {
             "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": {
@@ -7042,7 +7040,7 @@
             "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": [
                 {
@@ -7058,20 +7056,20 @@
                     "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": {
@@ -7126,7 +7124,7 @@
                 "mime-type"
             ],
             "support": {
-                "source": "https://github.com/symfony/mime/tree/v6.3.3"
+                "source": "https://github.com/symfony/mime/tree/v6.3.5"
             },
             "funding": [
                 {
@@ -7142,7 +7140,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-31T07:08:24+00:00"
+            "time": "2023-09-29T06:59:36+00:00"
         },
         {
             "name": "symfony/monolog-bridge",
@@ -7372,16 +7370,16 @@
         },
         {
             "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": {
@@ -7424,7 +7422,7 @@
                 "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": [
                 {
@@ -7440,89 +7438,7 @@
                     "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",
@@ -8175,16 +8091,16 @@
         },
         {
             "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": {
@@ -8238,7 +8154,7 @@
                 "url"
             ],
             "support": {
-                "source": "https://github.com/symfony/routing/tree/v6.3.3"
+                "source": "https://github.com/symfony/routing/tree/v6.3.5"
             },
             "funding": [
                 {
@@ -8254,7 +8170,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-31T07:08:24+00:00"
+            "time": "2023-09-20T16:05:51+00:00"
         },
         {
             "name": "symfony/runtime",
@@ -8337,16 +8253,16 @@
         },
         {
             "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": {
@@ -8363,7 +8279,7 @@
                 "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",
@@ -8427,7 +8343,7 @@
             "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": [
                 {
@@ -8443,20 +8359,20 @@
                     "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": {
@@ -8512,7 +8428,7 @@
             "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": [
                 {
@@ -8528,7 +8444,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-31T07:08:24+00:00"
+            "time": "2023-09-10T17:47:23+00:00"
         },
         {
             "name": "symfony/security-csrf",
@@ -8600,16 +8516,16 @@
         },
         {
             "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": {
@@ -8667,7 +8583,7 @@
             "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": [
                 {
@@ -8683,20 +8599,20 @@
                     "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": {
@@ -8761,7 +8677,7 @@
             "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": [
                 {
@@ -8777,7 +8693,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-24T14:35:28+00:00"
+            "time": "2023-09-29T16:18:53+00:00"
         },
         {
             "name": "symfony/service-contracts",
@@ -8925,16 +8841,16 @@
         },
         {
             "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": {
@@ -8991,7 +8907,7 @@
                 "utf8"
             ],
             "support": {
-                "source": "https://github.com/symfony/string/tree/v6.3.2"
+                "source": "https://github.com/symfony/string/tree/v6.3.5"
             },
             "funding": [
                 {
@@ -9007,7 +8923,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-05T08:41:27+00:00"
+            "time": "2023-09-18T10:38:32+00:00"
         },
         {
             "name": "symfony/translation",
@@ -9184,16 +9100,16 @@
         },
         {
             "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": {
@@ -9272,7 +9188,7 @@
             "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": [
                 {
@@ -9288,7 +9204,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-20T16:42:33+00:00"
+            "time": "2023-09-12T06:57:20+00:00"
         },
         {
             "name": "symfony/twig-bundle",
@@ -9377,16 +9293,16 @@
         },
         {
             "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": {
@@ -9453,7 +9369,7 @@
             "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": [
                 {
@@ -9469,20 +9385,20 @@
                     "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": {
@@ -9537,7 +9453,7 @@
                 "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": [
                 {
@@ -9553,7 +9469,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-24T14:51:05+00:00"
+            "time": "2023-09-12T10:11:35+00:00"
         },
         {
             "name": "symfony/var-exporter",
@@ -9631,31 +9547,31 @@
         },
         {
             "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": {
@@ -9682,7 +9598,7 @@
             "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": [
                 {
@@ -9698,7 +9614,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-05-31T14:28:33+00:00"
+            "time": "2023-09-26T14:41:29+00:00"
         },
         {
             "name": "symfony/yaml",
@@ -10391,16 +10307,16 @@
         },
         {
             "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": {
@@ -10463,24 +10379,24 @@
             ],
             "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": {
@@ -10522,7 +10438,7 @@
             ],
             "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": [
                 {
@@ -10538,7 +10454,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-17T09:50:14+00:00"
+            "time": "2023-10-11T07:11:09+00:00"
         },
         {
             "name": "composer/xdebug-handler",
@@ -10908,16 +10824,16 @@
         },
         {
             "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": {
@@ -10950,8 +10866,6 @@
                 "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"
             },
@@ -10991,7 +10905,7 @@
             ],
             "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": [
                 {
@@ -10999,7 +10913,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-09-17T14:37:54+00:00"
+            "time": "2023-10-12T13:47:26+00:00"
         },
         {
             "name": "masterminds/html5",
@@ -11237,16 +11151,16 @@
         },
         {
             "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": {
@@ -11295,7 +11209,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-09-13T09:49:47+00:00"
+            "time": "2023-10-17T15:46:26+00:00"
         },
         {
             "name": "phpstan/phpstan-doctrine",
@@ -11369,16 +11283,16 @@
         },
         {
             "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": {
@@ -11415,9 +11329,9 @@
             "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",
@@ -11470,22 +11384,22 @@
         },
         {
             "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"
@@ -11493,8 +11407,8 @@
             "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",
@@ -11505,7 +11419,8 @@
                 "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": {
@@ -11535,22 +11450,22 @@
             "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": {
@@ -11607,7 +11522,7 @@
             "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": [
                 {
@@ -11615,7 +11530,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-09-12T14:36:20+00:00"
+            "time": "2023-09-19T04:57:46+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",

programming_language: Jupyter Notebook
filename: doctrine.yaml
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,7 @@ doctrine:
                 default_table_options:
                     charset: utf8mb4
                     collate: utf8mb4_unicode_ci
+                schema_manager_factory: doctrine.dbal.default_schema_manager_factory
 
         types:
             datetime: App\Doctrine\UTCDateTimeType
@@ -26,6 +27,7 @@ doctrine:
         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

programming_language: Python
filename: framework.yaml
change_type: ModificationType.MODIFY
diff: @@ -17,8 +17,6 @@ framework:
         cookie_httponly: true
         storage_factory_id: session.storage.factory.native
 
-    #esi: ~
-    #fragments: ~
     php_errors:
         log: true
 

programming_language: Ruby
filename: kimai.yaml
change_type: ModificationType.MODIFY
diff: @@ -151,11 +151,13 @@ 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
 # --------------------------------------------------------------------------------
 
 

programming_language: Python
filename: security.yaml
change_type: ModificationType.MODIFY
diff: @@ -65,6 +65,12 @@ security:
                 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

programming_language: Markdown
filename: routes.yaml
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 controllers:
     resource: ../src/Controller/
-    type: annotation
+    type: attribute
     prefix: /{_locale}
     requirements:
         _locale: '%app_locales%'
@@ -19,17 +19,17 @@ api.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%'
@@ -38,7 +38,7 @@ security:
 
 kernel:
     resource: ../src/Kernel.php
-    type: annotation
+    type: attribute
 
 home:
     path: /

programming_language: CoffeeScript
filename: services.yaml
change_type: ModificationType.MODIFY
diff: @@ -91,16 +91,10 @@ services:
     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

programming_language: Python
filename: phpstan.neon
change_type: ModificationType.MODIFY
diff: @@ -928,7 +928,7 @@ parameters:
 
         -
             message: "#^Cannot access offset 'user' on mixed\\.$#"
-            count: 2
+            count: 1
             path: src/Controller/CalendarController.php
 
         -
@@ -2578,12 +2578,12 @@ parameters:
 
         -
             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
 
         -
@@ -3603,12 +3603,12 @@ parameters:
 
         -
             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
 
         -
@@ -4293,7 +4293,7 @@ parameters:
 
         -
             message: "#^Cannot access offset 'lastRecord' on mixed\\.$#"
-            count: 2
+            count: 1
             path: src/Project/ProjectStatisticService.php
 
         -
@@ -4438,7 +4438,7 @@ parameters:
 
         -
             message: "#^Cannot access offset 'project' on mixed\\.$#"
-            count: 3
+            count: 1
             path: src/Reporting/ProjectDetails/ProjectDetailsForm.php
 
         -
@@ -5716,11 +5716,6 @@ parameters:
             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

programming_language: CoffeeScript
filename: ActionsController.php
change_type: ModificationType.MODIFY
diff: @@ -21,10 +21,10 @@ 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')]

programming_language: PHP
filename: ActivityController.php
change_type: ModificationType.MODIFY
diff: @@ -26,8 +26,8 @@ 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;

programming_language: PHP
filename: BaseApiController.php
change_type: ModificationType.MODIFY
diff: @@ -10,12 +10,12 @@
 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;
 
@@ -30,13 +30,13 @@ abstract class BaseApiController extends AbstractController
 
     /**
      * @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')

programming_language: PHP
filename: CustomerController.php
change_type: ModificationType.MODIFY
diff: @@ -26,8 +26,8 @@ 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;

programming_language: PHP
filename: ProjectController.php
change_type: ModificationType.MODIFY
diff: @@ -27,8 +27,8 @@ 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;

programming_language: PHP
filename: TimesheetController.php
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,7 @@ 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;
@@ -40,7 +41,6 @@ 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')]
@@ -93,7 +93,7 @@ final class TimesheetController extends BaseApiController
     #[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
@@ -257,7 +257,8 @@ final class TimesheetController extends BaseApiController
         $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);

programming_language: PHP
filename: UserController.php
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ 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;
@@ -39,6 +39,7 @@ 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,
@@ -60,6 +61,7 @@ final class UserController extends BaseApiController
     #[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();
@@ -88,7 +90,13 @@ 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);
     }

programming_language: PHP
filename: ActivityService.php
change_type: ModificationType.MODIFY
diff: @@ -20,7 +20,7 @@ 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;
 
 /**

programming_language: PHP
filename: CalendarService.php
change_type: ModificationType.MODIFY
diff: @@ -18,7 +18,7 @@ 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
 {

programming_language: PHP
filename: DemoteUserCommand.php
change_type: ModificationType.MODIFY
diff: @@ -33,9 +33,6 @@ final class DemoteUserCommand extends AbstractRoleCommand
             );
     }
 
-    /**
-     * {@inheritdoc}
-     */
     protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
     {
         $username = $user->getUserIdentifier();

programming_language: PHP
filename: InvoiceCreateCommand.php
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@ 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;
@@ -32,7 +33,6 @@ 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

programming_language: PHP
filename: UserLoginLinkCommand.php
change_type: ModificationType.ADD
diff: @@ -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;
+    }
+}

programming_language: PHP
filename: Constants.php
change_type: ModificationType.MODIFY
diff: @@ -17,11 +17,11 @@ 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
      */
@@ -31,7 +31,7 @@ class Constants
      */
     public const GITHUB = 'https://github.com/kimai/kimai/';
     /**
-     * The Github repository name
+     * The GitHub repository name
      */
     public const GITHUB_REPO = 'kimai/kimai';
     /**

programming_language: PHP
filename: AbstractController.php
change_type: ModificationType.MODIFY
diff: @@ -17,7 +17,6 @@ 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;
@@ -76,13 +75,13 @@ abstract class AbstractController extends BaseAbstractController implements Serv
 
     /**
      * @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);
     }

programming_language: PHP
filename: ActivityController.php
change_type: ModificationType.MODIFY
diff: @@ -35,7 +35,7 @@ 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;

programming_language: PHP
filename: ContractController.php
change_type: ModificationType.MODIFY
diff: @@ -16,10 +16,10 @@ 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

programming_language: PHP
filename: CustomerController.php
change_type: ModificationType.MODIFY
diff: @@ -36,7 +36,7 @@ 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;

programming_language: PHP
filename: DashboardController.php
change_type: ModificationType.MODIFY
diff: @@ -16,7 +16,7 @@ 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;

programming_language: PHP
filename: HomepageController.php
change_type: ModificationType.MODIFY
diff: @@ -13,7 +13,7 @@ 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;

programming_language: PHP
filename: InvoiceController.php
change_type: ModificationType.MODIFY
diff: @@ -38,6 +38,7 @@ 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;
@@ -48,7 +49,6 @@ 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;
 
 /**
@@ -468,36 +468,6 @@ 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

programming_language: PHP
filename: PermissionController.php
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,7 @@ 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;

programming_language: PHP
filename: ProfileController.php
change_type: ModificationType.MODIFY
diff: @@ -32,8 +32,8 @@ 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;

programming_language: PHP
filename: ProjectController.php
change_type: ModificationType.MODIFY
diff: @@ -40,7 +40,7 @@ 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;

programming_language: PHP
filename: ReportUsersMonthController.php
change_type: ModificationType.MODIFY
diff: @@ -42,7 +42,7 @@ final class ReportUsersMonthController extends AbstractController
     {
         $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);

programming_language: PHP
filename: ReportUsersWeekController.php
change_type: ModificationType.MODIFY
diff: @@ -42,7 +42,7 @@ final class ReportUsersWeekController extends AbstractController
     {
         $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);

programming_language: PHP
filename: ReportUsersYearController.php
change_type: ModificationType.MODIFY
diff: @@ -19,7 +19,6 @@ 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;
@@ -30,11 +29,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
 #[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
     {
@@ -44,17 +38,12 @@ final class ReportUsersYearController extends AbstractController
         );
     }
 
-    /**
-     * @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);

programming_language: PHP
filename: UserMonthController.php
change_type: ModificationType.MODIFY
diff: @@ -10,10 +10,13 @@
 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;
@@ -35,6 +38,21 @@ final class UserMonthController extends AbstractUserReportController
         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();
@@ -95,6 +113,7 @@ final class UserMonthController extends AbstractUserReportController
             'current' => $start,
             'next' => $nextMonth,
             'previous' => $previousMonth,
+            'export_route' => 'report_user_month_export',
         ];
     }
 }

programming_language: PHP
filename: UserWeekController.php
change_type: ModificationType.MODIFY
diff: @@ -9,10 +9,13 @@
 
 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;
@@ -34,6 +37,21 @@ final class UserWeekController extends AbstractUserReportController
         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();
@@ -88,6 +106,7 @@ final class UserWeekController extends AbstractUserReportController
             'current' => $start,
             'next' => $next,
             'previous' => $previous,
+            'export_route' => 'report_user_week_export',
         ];
     }
 }

programming_language: PHP
filename: UserYearController.php
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,8 @@ 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;
@@ -18,6 +20,7 @@ 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;
@@ -39,6 +42,21 @@ final class UserYearController extends AbstractUserReportController
         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();
@@ -104,6 +122,7 @@ final class UserYearController extends AbstractUserReportController
             'current' => $start,
             'next' => $next,
             'previous' => $previous,
+            'export_route' => 'report_user_year_export',
         ];
     }
 

programming_language: PHP
filename: LoginLinkController.php
change_type: ModificationType.ADD
diff: @@ -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();
+    }
+}

programming_language: PHP
filename: PasswordResetController.php
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,7 @@ 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;
@@ -26,7 +27,6 @@ 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')]
@@ -108,7 +108,7 @@ final class PasswordResetController extends AbstractController
         }
 
         return $this->render('security/password-reset/check_email.html.twig', [
-            'tokenLifetime' => ceil($this->configuration->getPasswordResetRetryLifetime() / 3600),
+            'tokenLifetime' => $this->configuration->getPasswordResetRetryLifetime(),
         ]);
     }
 

programming_language: PHP
filename: SelfRegistrationController.php
change_type: ModificationType.MODIFY
diff: @@ -17,6 +17,7 @@ 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;
@@ -27,7 +28,6 @@ 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')]

programming_language: PHP
filename: SystemConfigurationController.php
change_type: ModificationType.MODIFY
diff: @@ -36,7 +36,7 @@ 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;

programming_language: PHP
filename: TimesheetAbstractController.php
change_type: ModificationType.MODIFY
diff: @@ -33,7 +33,7 @@ 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;
@@ -334,6 +334,13 @@ abstract class TimesheetAbstractController extends AbstractController
             $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);

programming_language: PHP
filename: UserController.php
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ 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;

programming_language: PHP
filename: WizardController.php
change_type: ModificationType.MODIFY
diff: @@ -14,6 +14,7 @@ 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;
@@ -58,6 +59,11 @@ final class WizardController extends AbstractController
                 ->setMethod('POST')
                 ->getForm();
 
+            $next = 'done';
+            if ($user->requiresPasswordReset()) {
+                $next = 'password';
+            }
+
             $form->handleRequest($request);
 
             if ($form->isSubmitted() && $form->isValid()) {
@@ -72,13 +78,44 @@ final class WizardController extends AbstractController
                 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(),
             ]);

programming_language: PHP
filename: CustomerService.php
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,7 @@ 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

programming_language: PHP
filename: Configuration.php
change_type: ModificationType.MODIFY
diff: @@ -343,7 +343,7 @@ final class Configuration implements ConfigurationInterface
                     ->defaultValue('{Y}/{cy,3}')
                 ->end()
                 ->booleanNode('upload_twig')
-                    ->defaultTrue()
+                    ->defaultFalse()
                 ->end()
             ->end()
         ;

programming_language: PHP
filename: ModifiedSubscriber.php
change_type: ModificationType.ADD
diff: @@ -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);
+        }
+    }
+}

programming_language: PHP
filename: TimesheetSubscriber.php
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,7 @@ 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;
@@ -18,6 +19,7 @@ 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
 {
     /**

programming_language: PHP
filename: Activity.php
change_type: ModificationType.MODIFY
diff: @@ -44,7 +44,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
     #[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')]
@@ -87,7 +87,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
      *
      * @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>')]
@@ -102,7 +102,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
     #[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'))]

programming_language: PHP
filename: ActivityMeta.php
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ 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;
 

programming_language: PHP
filename: ActivityRate.php
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ 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;
 

programming_language: PHP
filename: Bookmark.php
change_type: ModificationType.MODIFY
diff: @@ -29,7 +29,7 @@ class Bookmark
     #[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)]

programming_language: PHP
filename: CommentTableTypeTrait.php
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,7 @@ trait CommentTableTypeTrait
     #[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)]

programming_language: PHP
filename: Customer.php
change_type: ModificationType.MODIFY
diff: @@ -156,7 +156,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
      *
      * @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>')]
@@ -171,7 +171,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
     #[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'))]
@@ -180,7 +180,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
      * 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;

programming_language: Rust
filename: CustomerComment.php
change_type: ModificationType.MODIFY
diff: @@ -21,7 +21,7 @@ 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;
 

programming_language: PHP
filename: CustomerMeta.php
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ 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;
 

programming_language: PHP
filename: CustomerRate.php
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ 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;
 

programming_language: PHP
filename: Invoice.php
change_type: ModificationType.MODIFY
diff: @@ -56,11 +56,11 @@ class Invoice implements EntityWithMetaFields
     #[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)]
@@ -109,7 +109,7 @@ class Invoice implements EntityWithMetaFields
      *
      * @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>')]

programming_language: PHP
filename: InvoiceMeta.php
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ 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;
 

programming_language: PHP
filename: Project.php
change_type: ModificationType.MODIFY
diff: @@ -48,7 +48,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
      * 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'])]
@@ -137,7 +137,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
      *
      * @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>')]
@@ -152,7 +152,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
     #[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'))]

programming_language: PHP
filename: ProjectComment.php
change_type: ModificationType.MODIFY
diff: @@ -21,7 +21,7 @@ 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;
 

programming_language: PHP
filename: ProjectMeta.php
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ 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;
 

programming_language: PHP
filename: ProjectRate.php
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ 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;
 

programming_language: PHP
filename: Rate.php
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ trait Rate
     #[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')]

programming_language: PHP
filename: RolePermission.php
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ class RolePermission
     #[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)]

programming_language: PHP
filename: Tag.php
change_type: ModificationType.MODIFY
diff: @@ -39,7 +39,7 @@ class Tag
     #[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;

programming_language: PHP
filename: Team.php
change_type: ModificationType.MODIFY
diff: @@ -47,7 +47,7 @@ class Team
      *
      * @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]
@@ -59,7 +59,7 @@ class 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'))]
@@ -69,7 +69,7 @@ class 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'))]
@@ -79,7 +79,7 @@ class 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'))]

programming_language: PHP
filename: TeamMember.php
change_type: ModificationType.MODIFY
diff: @@ -26,14 +26,14 @@ class TeamMember
     #[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'])]

programming_language: PHP
filename: Timesheet.php
change_type: ModificationType.MODIFY
diff: @@ -15,7 +15,6 @@ 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;
@@ -128,21 +127,21 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
     #[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'])]
@@ -189,12 +188,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
     #[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
      *
@@ -211,7 +206,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
      *
      * @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>')]
@@ -223,6 +218,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
     {
         $this->tags = new ArrayCollection();
         $this->meta = new ArrayCollection();
+        $this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
     }
 
     /**
@@ -590,11 +586,16 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
         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[]
      */
@@ -687,7 +688,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
         }
 
         // 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;

programming_language: Rust
filename: TimesheetMeta.php
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ 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;
 

programming_language: PHP
filename: User.php
change_type: ModificationType.MODIFY
diff: @@ -42,9 +42,9 @@ use Symfony\Component\Validator\Constraints as Assert;
 #[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
 {
@@ -127,15 +127,15 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
      *
      * @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'])]
@@ -217,7 +217,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
     #[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')]
@@ -297,6 +297,14 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
         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;
@@ -836,9 +844,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
         return $this->email !== null;
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getPassword(): ?string
     {
         return $this->password;
@@ -864,9 +869,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
         return $this->confirmationToken;
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getRoles(): array
     {
         $roles = $this->roles;
@@ -963,7 +965,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
         return $this;
     }
 
-    public function setPasswordRequestedAt(\DateTime $date = null): User
+    public function setPasswordRequestedAt(?\DateTime $date = null): User
     {
         $this->passwordRequestedAt = $date;
 
@@ -1113,6 +1115,20 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
         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__');

programming_language: Rust
filename: InvoiceDocumentSubscriber.php
change_type: ModificationType.MODIFY
diff: @@ -45,10 +45,6 @@ final class InvoiceDocumentSubscriber extends AbstractActionsSubscriber
 
         $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);
         }

programming_language: PHP
filename: MenuBuilderSubscriber.php
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,8 @@ 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;
 
 /**

programming_language: PHP
filename: UserPreferenceSubscriber.php
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ 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;

programming_language: PHP
filename: UserProfileSubscriber.php
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ 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;

programming_language: PHP
filename: WizardSubscriber.php
change_type: ModificationType.MODIFY
diff: @@ -49,29 +49,32 @@ 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);
+        }
     }
 }

programming_language: PHP
filename: AbstractSpreadsheetRenderer.php
change_type: ModificationType.MODIFY
diff: @@ -30,8 +30,8 @@ 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;

programming_language: PHP
filename: HtmlRenderer.php
change_type: ModificationType.MODIFY
diff: @@ -21,9 +21,11 @@ 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
 {
@@ -93,6 +95,11 @@ class HtmlRenderer
 
         $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,

programming_language: PHP
filename: PDFRenderer.php
change_type: ModificationType.MODIFY
diff: @@ -16,8 +16,10 @@ 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
 {
@@ -76,6 +78,12 @@ class PDFRenderer implements DispositionInlineInterface
         $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,

programming_language: PHP
filename: HtmlRendererFactory.php
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ 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

programming_language: PHP
filename: ServiceExport.php
change_type: ModificationType.MODIFY
diff: @@ -14,7 +14,7 @@ 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
 {

programming_language: PHP
filename: MetaFieldExtractor.php
change_type: ModificationType.MODIFY
diff: @@ -12,7 +12,7 @@ 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

programming_language: PHP
filename: UserPreferenceExtractor.php
change_type: ModificationType.MODIFY
diff: @@ -12,7 +12,7 @@ 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

programming_language: PHP
filename: TimesheetMultiUpdate.php
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,7 @@ 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;
@@ -141,9 +142,12 @@ final class TimesheetMultiUpdate extends AbstractType
             'label' => false,
             'required' => true,
             'expanded' => true,
+            'label_attr' => [
+                'class' => 'radio-inline',
+            ],
             'choices' => [
-                'replaceTags' => true,
-                'appendTags' => false,
+                'append' => false,
+                'replace' => true,
             ]
         ]);
 
@@ -151,6 +155,23 @@ final class TimesheetMultiUpdate extends AbstractType
             '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,

programming_language: PHP
filename: TimesheetMultiUpdateDTO.php
change_type: ModificationType.MODIFY
diff: @@ -49,6 +49,8 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit
      * @var array<string>
      */
     private array $updateMeta = [];
+    private bool $replaceDescription = false;
+    private ?string $description = null;
 
     public function __construct()
     {
@@ -98,6 +100,16 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit
         $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;
@@ -148,6 +160,16 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit
         $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;

programming_language: PHP
filename: MenuChoiceType.php
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ 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;

programming_language: PHP
filename: SkinType.php
change_type: ModificationType.MODIFY
diff: @@ -26,6 +26,7 @@ final class SkinType extends AbstractType
     public function configureOptions(OptionsResolver $resolver): void
     {
         $resolver->setDefaults([
+            'label' => 'skin',
             'search' => false,
             'required' => true,
             'choices' => self::THEMES,

programming_language: PHP
filename: UserCreateType.php
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,7 @@ 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;
@@ -63,6 +64,12 @@ class UserCreateType extends UserEditType
                 'required' => false,
             ]);
         }
+
+        $builder->add('requiresPasswordReset', YesNoType::class, [
+            'label' => 'force_password_change',
+            'help' => 'force_password_change_help',
+            'required' => false,
+        ]);
     }
 
     public function configureOptions(OptionsResolver $resolver): void

programming_language: PHP
filename: ServiceInvoice.php
change_type: ModificationType.MODIFY
diff: @@ -22,9 +22,9 @@ 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.

programming_language: PHP
filename: Kernel.php
change_type: ModificationType.MODIFY
diff: @@ -169,7 +169,7 @@ class Kernel extends BaseKernel
         $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');

programming_language: PHP
filename: ProjectService.php
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,7 @@ 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;
 
 /**

programming_language: PHP
filename: ProjectStatisticService.php
change_type: ModificationType.MODIFY
diff: @@ -36,7 +36,7 @@ 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

programming_language: PHP
filename: ReportingService.php
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ 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

programming_language: PHP
filename: TimesheetRepository.php
change_type: ModificationType.MODIFY
diff: @@ -355,14 +355,17 @@ class TimesheetRepository extends EntityRepository
     {
         $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();

programming_language: PHP
filename: TwoFactorCondition.php
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,6 @@
 
 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;
@@ -22,9 +21,6 @@ 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;

programming_language: PHP
filename: TimesheetService.php
change_type: ModificationType.MODIFY
diff: @@ -31,7 +31,7 @@ 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;

programming_language: PHP
filename: Extensions.php
change_type: ModificationType.MODIFY
diff: @@ -21,9 +21,6 @@ final class Extensions extends AbstractExtension
 {
     public const REPORT_DATE = 'Y-m-d';
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFilters(): array
     {
         return [
@@ -37,9 +34,6 @@ final class Extensions extends AbstractExtension
         ];
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFunctions(): array
     {
         return [

programming_language: PHP
filename: LocaleFormatExtensions.php
change_type: ModificationType.MODIFY
diff: @@ -33,9 +33,6 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
     {
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFilters(): array
     {
         return [
@@ -74,9 +71,6 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
         ];
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFunctions(): array
     {
         return [

programming_language: PHP
filename: PaginationExtension.php
change_type: ModificationType.MODIFY
diff: @@ -27,9 +27,6 @@ final class PaginationExtension extends AbstractExtension
     {
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getFunctions(): array
     {
         return [

programming_language: PHP
filename: ChainPolicy.php
change_type: ModificationType.ADD
diff: @@ -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);
+        }
+    }
+}

programming_language: PHP
filename: DefaultPolicy.php
change_type: ModificationType.ADD
diff: @@ -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
+    {
+    }
+}

programming_language: PHP
filename: ExportPolicy.php
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: PHP
filename: ForbiddenPolicy.php
change_type: ModificationType.ADD
diff: @@ -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);
+        }
+    }
+}

programming_language: PHP
filename: InvoicePolicy.php
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: PHP
filename: TwigRendererTrait.php
change_type: ModificationType.MODIFY
diff: @@ -9,9 +9,11 @@
 
 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
@@ -30,6 +32,13 @@ trait TwigRendererTrait
             $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) {

programming_language: PHP
filename: LoginManager.php
change_type: ModificationType.MODIFY
diff: @@ -12,12 +12,12 @@ 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
 {

programming_language: PHP
filename: UserService.php
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,7 @@ 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;
 

programming_language: PHP
filename: AbstractActiveUsers.php
change_type: ModificationType.MODIFY
diff: @@ -12,8 +12,8 @@ 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
     {

programming_language: PHP
filename: AbstractAmountPeriod.php
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ 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
 {
@@ -30,8 +30,8 @@ abstract class AbstractAmountPeriod extends AbstractWidget
     }
 
     /**
-     * @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
     {
@@ -41,7 +41,7 @@ abstract class AbstractAmountPeriod extends AbstractWidget
     }
 
     /**
-     * @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

programming_language: PHP
filename: AbstractCounterDuration.php
change_type: ModificationType.MODIFY
diff: @@ -12,8 +12,8 @@ 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
     {

programming_language: PHP
filename: AbstractCounterYear.php
change_type: ModificationType.MODIFY
diff: @@ -21,7 +21,7 @@ abstract class AbstractCounterYear extends AbstractWidgetType
     }
 
     /**
-     * @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
     {
@@ -39,7 +39,7 @@ abstract class AbstractCounterYear extends AbstractWidgetType
     }
 
     /**
-     * @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;
 

programming_language: PHP
filename: AbstractUserRevenuePeriod.php
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ 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
 {
@@ -35,8 +35,8 @@ abstract class AbstractUserRevenuePeriod extends AbstractWidget
     }
 
     /**
-     * @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
     {
@@ -46,7 +46,7 @@ abstract class AbstractUserRevenuePeriod extends AbstractWidget
     }
 
     /**
-     * @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

programming_language: PHP
filename: AbstractWidget.php
change_type: ModificationType.MODIFY
diff: @@ -68,8 +68,8 @@ abstract class AbstractWidget implements WidgetInterface
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: ActiveTimesheets.php
change_type: ModificationType.MODIFY
diff: @@ -9,6 +9,7 @@
 
 namespace App\Widget\Type;
 
+use App\Repository\Query\TimesheetQuery;
 use App\Repository\TimesheetRepository;
 use App\Widget\WidgetException;
 use App\Widget\WidgetInterface;
@@ -20,16 +21,25 @@ final class ActiveTimesheets 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(['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'];
     }
 
@@ -44,7 +54,7 @@ final class ActiveTimesheets extends AbstractWidgetType
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: ActiveUsersMonth.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class ActiveUsersMonth extends AbstractActiveUsers
     }
 
     /**
-     * @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
     {
@@ -34,7 +34,7 @@ final class ActiveUsersMonth extends AbstractActiveUsers
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: ActiveUsersToday.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class ActiveUsersToday extends AbstractActiveUsers
     }
 
     /**
-     * @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
     {
@@ -34,7 +34,7 @@ final class ActiveUsersToday extends AbstractActiveUsers
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: ActiveUsersTotal.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class ActiveUsersTotal extends AbstractActiveUsers
     }
 
     /**
-     * @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
     {
@@ -34,7 +34,7 @@ final class ActiveUsersTotal extends AbstractActiveUsers
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: ActiveUsersWeek.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class ActiveUsersWeek extends AbstractActiveUsers
     }
 
     /**
-     * @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
     {
@@ -34,7 +34,7 @@ final class ActiveUsersWeek extends AbstractActiveUsers
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: ActiveUsersYear.php
change_type: ModificationType.MODIFY
diff: @@ -22,8 +22,8 @@ final class ActiveUsersYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {
@@ -34,7 +34,7 @@ final class ActiveUsersYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: AmountMonth.php
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,8 @@ 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
     {
@@ -28,7 +28,7 @@ final class AmountMonth extends AbstractAmountPeriod
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: AmountToday.php
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,8 @@ 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
     {
@@ -28,7 +28,7 @@ final class AmountToday extends AbstractAmountPeriod
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: AmountTotal.php
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,8 @@ 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
     {
@@ -33,7 +33,7 @@ final class AmountTotal extends AbstractAmountPeriod
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: AmountWeek.php
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,8 @@ 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
     {
@@ -28,7 +28,7 @@ final class AmountWeek extends AbstractAmountPeriod
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: AmountYear.php
change_type: ModificationType.MODIFY
diff: @@ -15,7 +15,7 @@ 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
 {
@@ -25,8 +25,8 @@ final class AmountYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {
@@ -37,7 +37,7 @@ final class AmountYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: DailyWorkingTimeChart.php
change_type: ModificationType.MODIFY
diff: @@ -46,8 +46,8 @@ final class DailyWorkingTimeChart extends AbstractWidget
     }
 
     /**
-     * @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
     {
@@ -61,7 +61,7 @@ final class DailyWorkingTimeChart extends AbstractWidget
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: DurationMonth.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class DurationMonth extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {
@@ -44,7 +44,7 @@ final class DurationMonth extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: DurationToday.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class DurationToday extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {
@@ -44,7 +44,7 @@ final class DurationToday extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: DurationTotal.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class DurationTotal extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {
@@ -44,7 +44,7 @@ final class DurationTotal extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: DurationWeek.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class DurationWeek extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {
@@ -44,7 +44,7 @@ final class DurationWeek extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: DurationYear.php
change_type: ModificationType.MODIFY
diff: @@ -22,8 +22,8 @@ final class DurationYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {
@@ -34,7 +34,7 @@ final class DurationYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: PaginatedWorkingTimeChart.php
change_type: ModificationType.MODIFY
diff: @@ -48,8 +48,8 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
     }
 
     /**
-     * @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
     {
@@ -87,7 +87,7 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: TotalsActivity.php
change_type: ModificationType.MODIFY
diff: @@ -25,8 +25,8 @@ final class TotalsActivity extends AbstractWidget
     }
 
     /**
-     * @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
     {
@@ -38,7 +38,7 @@ final class TotalsActivity extends AbstractWidget
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: TotalsCustomer.php
change_type: ModificationType.MODIFY
diff: @@ -25,8 +25,8 @@ final class TotalsCustomer extends AbstractWidget
     }
 
     /**
-     * @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
     {
@@ -38,7 +38,7 @@ final class TotalsCustomer extends AbstractWidget
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: TotalsProject.php
change_type: ModificationType.MODIFY
diff: @@ -25,8 +25,8 @@ final class TotalsProject extends AbstractWidget
     }
 
     /**
-     * @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
     {
@@ -38,7 +38,7 @@ final class TotalsProject extends AbstractWidget
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: TotalsUser.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class TotalsUser extends AbstractWidget
     }
 
     /**
-     * @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
     {
@@ -33,7 +33,7 @@ final class TotalsUser extends AbstractWidget
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserAmountMonth.php
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,8 @@ 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
     {
@@ -28,7 +28,7 @@ final class UserAmountMonth extends AbstractUserRevenuePeriod
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserAmountToday.php
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,8 @@ 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
     {
@@ -28,7 +28,7 @@ final class UserAmountToday extends AbstractUserRevenuePeriod
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserAmountTotal.php
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,8 @@ 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
     {
@@ -28,7 +28,7 @@ final class UserAmountTotal extends AbstractUserRevenuePeriod
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserAmountWeek.php
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,8 @@ 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
     {
@@ -28,7 +28,7 @@ final class UserAmountWeek extends AbstractUserRevenuePeriod
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserAmountYear.php
change_type: ModificationType.MODIFY
diff: @@ -15,7 +15,7 @@ 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
 {
@@ -45,8 +45,8 @@ final class UserAmountYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {
@@ -57,7 +57,7 @@ final class UserAmountYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserDurationMonth.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class UserDurationMonth extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {
@@ -39,7 +39,7 @@ final class UserDurationMonth extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserDurationToday.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class UserDurationToday extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {
@@ -39,7 +39,7 @@ final class UserDurationToday extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserDurationTotal.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class UserDurationTotal extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {
@@ -39,7 +39,7 @@ final class UserDurationTotal extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserDurationWeek.php
change_type: ModificationType.MODIFY
diff: @@ -20,8 +20,8 @@ final class UserDurationWeek extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {
@@ -39,7 +39,7 @@ final class UserDurationWeek extends AbstractCounterDuration
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserDurationYear.php
change_type: ModificationType.MODIFY
diff: @@ -32,8 +32,8 @@ final class UserDurationYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {
@@ -44,7 +44,7 @@ final class UserDurationYear extends AbstractCounterYear
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserTeamProjects.php
change_type: ModificationType.MODIFY
diff: @@ -60,7 +60,7 @@ final class UserTeamProjects extends AbstractWidget
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: UserTeams.php
change_type: ModificationType.MODIFY
diff: @@ -53,7 +53,7 @@ final class UserTeams extends AbstractWidget
     }
 
     /**
-     * @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
     {

programming_language: PHP
filename: WidgetInterface.php
change_type: ModificationType.MODIFY
diff: @@ -71,7 +71,7 @@ interface WidgetInterface
      * 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;
@@ -85,8 +85,8 @@ interface WidgetInterface
      * 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;
 

programming_language: PHP
filename: WorkingTimeService.php
change_type: ModificationType.MODIFY
diff: @@ -20,7 +20,7 @@ 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!

programming_language: PHP
filename: symfony.lock
change_type: ModificationType.MODIFY
diff: @@ -511,9 +511,6 @@
             "ref": "179470cb6492db92dffee208cfdb436f175c93b4"
         }
     },
-    "symfony/polyfill-ctype": {
-        "version": "v1.8.0"
-    },
     "symfony/polyfill-intl-grapheme": {
         "version": "v1.13.1"
     },

programming_language: Jupyter Notebook
filename: report_by_user.html.twig
change_type: ModificationType.MODIFY
diff: @@ -7,6 +7,7 @@
                 {{ 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 %}

programming_language: HTML
filename: report_by_user_data.html.twig
change_type: ModificationType.MODIFY
diff: @@ -17,10 +17,12 @@
 <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}) %}

programming_language: HTML
filename: report_by_user_layout.html.twig
change_type: ModificationType.MODIFY
diff: @@ -9,6 +9,8 @@
         {{ 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 %}

programming_language: HTML
filename: report_by_user_year.html.twig
change_type: ModificationType.MODIFY
diff: @@ -10,6 +10,7 @@
                 </a>
             </th>
         {% endblock %}
+        {% block upper_left %}&nbsp;{% endblock %}
         {% block column_classes_project %}{% endblock %}
         {% block column_classes_activity %}{% endblock %}
         {% block column_classes_total %}{% endblock %}

programming_language: HTML
filename: report_by_user_year_export.html.twig
change_type: ModificationType.ADD
diff: @@ -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 %}

programming_language: HTML
filename: check_email.html.twig
change_type: ModificationType.MODIFY
diff: @@ -3,7 +3,7 @@
 {% block login_form %}
 
     <p>
-        {{ 'resetting.check_email'|trans({'%tokenLifetime%': tokenLifetime})|nl2br }}
+        {{ 'resetting.check_email'|trans({'%tokenLifetime%': tokenLifetime|duration})|nl2br }}
     </p>
 
 {% endblock %}

programming_language: HTML
filename: multi-update.html.twig
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,11 @@
                     </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>

programming_language: HTML
filename: api-token.html.twig
change_type: ModificationType.MODIFY
diff: @@ -13,7 +13,11 @@
 
     <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>

programming_language: HTML
filename: widget-counter.html.twig
change_type: ModificationType.MODIFY
diff: @@ -7,12 +7,14 @@
     <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>
@@ -21,7 +23,6 @@
                         {{ data }}
                     {% endblock %}
                 </div>
-                {% if not url is empty %}</a>{% endif %}
             </div>
         </div>
     </div>

programming_language: HTML
filename: layout.html.twig
change_type: ModificationType.MODIFY
diff: @@ -20,11 +20,13 @@
 {% 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 %}
@@ -33,7 +35,7 @@
     {% 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 %}

programming_language: HTML
filename: password.html.twig
change_type: ModificationType.ADD
diff: @@ -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 %}

programming_language: HTML
filename: APIControllerBaseTest.php
change_type: ModificationType.MODIFY
diff: @@ -340,6 +340,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
                     'id' => 'int',
                     'username' => 'string',
                     'enabled' => 'bool',
+                    'apiToken' => 'bool',
                     'color' => '@string',
                     'alias' => '@string',
                     'accountNumber' => '@string',
@@ -353,6 +354,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
                     'id' => 'int',
                     'username' => 'string',
                     'enabled' => 'bool',
+                    'apiToken' => 'bool',
                     'alias' => '@string',
                     'title' => '@string',
                     'supervisor' => ['result' => 'object', 'type' => '@UserEntity'],

programming_language: PHP
filename: UserControllerTest.php
change_type: ModificationType.MODIFY
diff: @@ -45,7 +45,9 @@ class UserControllerTest extends APIControllerBaseTest
     {
         $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);
@@ -55,11 +57,30 @@ class UserControllerTest extends APIControllerBaseTest
         }
     }
 
+    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);
@@ -73,7 +94,9 @@ class UserControllerTest extends APIControllerBaseTest
     {
         $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);
@@ -87,7 +110,9 @@ class UserControllerTest extends APIControllerBaseTest
     {
         $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);
@@ -100,7 +125,9 @@ class UserControllerTest extends APIControllerBaseTest
     {
         $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);
@@ -124,7 +151,9 @@ class UserControllerTest extends APIControllerBaseTest
     {
         $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);
@@ -150,7 +179,9 @@ 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']);
@@ -238,7 +269,9 @@ 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);
         self::assertFalse($result['enabled']);
 
         $data = [
@@ -253,7 +286,9 @@ 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']);

programming_language: PHP
filename: UserLoginLinkCommandTest.php
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: PHP
filename: ConfigurationTest.php
change_type: ModificationType.MODIFY
diff: @@ -309,7 +309,7 @@ class ConfigurationTest extends TestCase
                     1 => 'templates/invoice/renderer/',
                 ],
                 'number_format' => '{Y}/{cy,3}',
-                'upload_twig' => true,
+                'upload_twig' => false,
             ],
             'export' => [
                 'documents' => [

programming_language: PHP
filename: TimesheetTest.php
change_type: ModificationType.MODIFY
diff: @@ -34,7 +34,7 @@ class TimesheetTest extends TestCase
         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));
@@ -179,7 +179,7 @@ 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');
 
@@ -210,7 +210,7 @@ class TimesheetTest extends TestCase
         $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());

programming_language: PHP
filename: PdfRendererTest.php
change_type: ModificationType.MODIFY
diff: @@ -15,6 +15,7 @@ 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;
 
 /**
@@ -25,7 +26,7 @@ use Twig\Environment;
  */
 class PdfRendererTest extends AbstractRendererTest
 {
-    public function testConfiguration()
+    public function testConfiguration(): void
     {
         $sut = new PDFRenderer(
             $this->createMock(Environment::class),
@@ -43,12 +44,14 @@ class PdfRendererTest extends AbstractRendererTest
         $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();
@@ -63,9 +66,30 @@ class PdfRendererTest extends AbstractRendererTest
         $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());
     }
 }

programming_language: PHP
filename: HtmlRendererTest.php
change_type: ModificationType.MODIFY
diff: @@ -14,6 +14,7 @@ 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;
 
 /**
@@ -22,7 +23,7 @@ use Twig\Environment;
  */
 class HtmlRendererTest extends AbstractRendererTest
 {
-    public function testConfiguration()
+    public function testConfiguration(): void
     {
         $sut = new HtmlRenderer(
             $this->createMock(Environment::class),
@@ -34,11 +35,12 @@ 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');

programming_language: PHP
filename: PdfRendererTest.php
change_type: ModificationType.MODIFY
diff: @@ -15,6 +15,7 @@ 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;
 
 /**
@@ -25,7 +26,7 @@ use Twig\Environment;
  */
 class PdfRendererTest extends AbstractRendererTest
 {
-    public function testConfiguration()
+    public function testConfiguration(): void
     {
         $sut = new PDFRenderer(
             $this->createMock(Environment::class),
@@ -36,12 +37,14 @@ class PdfRendererTest extends AbstractRendererTest
         $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();

programming_language: PHP
filename: PdfRendererTest.php
change_type: ModificationType.MODIFY
diff: @@ -10,11 +10,13 @@
 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;
@@ -28,7 +30,7 @@ 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));
@@ -40,11 +42,12 @@ class PdfRendererTest extends KernelTestCase
         $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');
 
@@ -64,9 +67,83 @@ class PdfRendererTest extends KernelTestCase
         $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());
+        }
     }
 }

programming_language: PHP
filename: TwigRendererTest.php
change_type: ModificationType.MODIFY
diff: @@ -10,8 +10,10 @@
 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;
 
@@ -24,7 +26,7 @@ class TwigRendererTest extends KernelTestCase
 {
     use RendererTestTrait;
 
-    public function testSupports()
+    public function testSupports(): void
     {
         $loader = new FilesystemLoader();
         $env = new Environment($loader);
@@ -38,11 +40,12 @@ class TwigRendererTest extends KernelTestCase
         $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');
@@ -55,7 +58,7 @@ class TwigRendererTest extends KernelTestCase
         $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);
@@ -75,4 +78,53 @@ class TwigRendererTest extends KernelTestCase
     "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());
+        }
+    }
 }

programming_language: PHP
filename: default.pdf.twig
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,5 @@
 <!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>

programming_language: HTML
filename: phpstan.neon
change_type: ModificationType.MODIFY
diff: @@ -1237,11 +1237,6 @@ parameters:
             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
@@ -1673,7 +1668,7 @@ parameters:
             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
 
@@ -5177,26 +5172,6 @@ parameters:
             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
@@ -5632,46 +5607,11 @@ parameters:
             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
@@ -6457,16 +6397,6 @@ parameters:
             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
@@ -6474,34 +6404,14 @@ parameters:
 
         -
             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

programming_language: CoffeeScript
filename: messages.ar.xlf
change_type: ModificationType.MODIFY
diff: @@ -756,10 +756,6 @@
         <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>
@@ -804,10 +800,6 @@
         <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>

programming_language: HTML
filename: messages.cs.xlf
change_type: ModificationType.MODIFY
diff: @@ -886,13 +886,13 @@
         <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>

programming_language: HTML
filename: messages.da.xlf
change_type: ModificationType.MODIFY
diff: @@ -349,13 +349,13 @@
         <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

programming_language: HTML
filename: messages.de.xlf
change_type: ModificationType.MODIFY
diff: @@ -523,13 +523,13 @@
         <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">
@@ -1625,6 +1625,14 @@
         <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>

programming_language: HTML
filename: messages.de_CH.xlf
change_type: ModificationType.MODIFY
diff: @@ -573,13 +573,13 @@
         <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>

programming_language: TypeScript
filename: messages.el.xlf
change_type: ModificationType.MODIFY
diff: @@ -441,13 +441,13 @@
         <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

programming_language: HTML
filename: messages.en.xlf
change_type: ModificationType.MODIFY
diff: @@ -523,13 +523,13 @@
         <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">
@@ -1625,6 +1625,14 @@
         <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>

programming_language: HTML
filename: messages.eo.xlf
change_type: ModificationType.MODIFY
diff: @@ -373,13 +373,13 @@
         <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

programming_language: HTML
filename: messages.es.xlf
change_type: ModificationType.MODIFY
diff: @@ -910,9 +910,9 @@
         <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>
@@ -958,10 +958,6 @@
         <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>

programming_language: HTML
filename: messages.eu.xlf
change_type: ModificationType.MODIFY
diff: @@ -937,13 +937,13 @@
         <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>

programming_language: HTML
filename: messages.fa.xlf
change_type: ModificationType.MODIFY
diff: @@ -419,14 +419,6 @@
         <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>

programming_language: HTML
filename: messages.fi.xlf
change_type: ModificationType.MODIFY
diff: @@ -405,13 +405,13 @@
         <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

programming_language: TypeScript
filename: messages.fo.xlf
change_type: ModificationType.MODIFY
diff: @@ -759,13 +759,13 @@
         <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>

programming_language: TypeScript
filename: messages.fr.xlf
change_type: ModificationType.MODIFY
diff: @@ -766,13 +766,13 @@
         <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>

programming_language: HTML
filename: messages.he.xlf
change_type: ModificationType.MODIFY
diff: @@ -366,14 +366,6 @@
         <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>

programming_language: HTML
filename: messages.hr.xlf
change_type: ModificationType.MODIFY
diff: @@ -453,13 +453,13 @@
         <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

programming_language: HTML
filename: messages.hu.xlf
change_type: ModificationType.MODIFY
diff: @@ -966,14 +966,6 @@
         <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>

programming_language: HTML
filename: messages.it.xlf
change_type: ModificationType.MODIFY
diff: @@ -405,13 +405,13 @@
         <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

programming_language: HTML
filename: messages.ko.xlf
change_type: ModificationType.MODIFY
diff: @@ -850,14 +850,6 @@
         <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>

programming_language: TypeScript
filename: messages.nb_NO.xlf
change_type: ModificationType.MODIFY
diff: @@ -234,10 +234,6 @@
         <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
             -->
@@ -624,10 +620,6 @@
         <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>

programming_language: TypeScript
filename: messages.nl.xlf
change_type: ModificationType.MODIFY
diff: @@ -942,14 +942,6 @@
         <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>

programming_language: HTML
filename: messages.pl.xlf
change_type: ModificationType.MODIFY
diff: @@ -361,13 +361,13 @@
         <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

programming_language: HTML
filename: messages.pt.xlf
change_type: ModificationType.MODIFY
diff: @@ -862,13 +862,13 @@
         <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>

programming_language: HTML
filename: messages.pt_BR.xlf
change_type: ModificationType.MODIFY
diff: @@ -806,10 +806,6 @@
         <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>
@@ -930,10 +926,6 @@
         <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>

programming_language: HTML
filename: messages.ro.xlf
change_type: ModificationType.MODIFY
diff: @@ -346,14 +346,6 @@
         <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>

programming_language: TypeScript
filename: messages.ru.xlf
change_type: ModificationType.MODIFY
diff: @@ -712,13 +712,13 @@
         <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>

programming_language: TypeScript
filename: messages.sk.xlf
change_type: ModificationType.MODIFY
diff: @@ -357,13 +357,13 @@
         <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

programming_language: HTML
filename: messages.sv.xlf
change_type: ModificationType.MODIFY
diff: @@ -938,13 +938,13 @@
         <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>

programming_language: TypeScript
filename: messages.tr.xlf
change_type: ModificationType.MODIFY
diff: @@ -906,14 +906,6 @@
         <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>

programming_language: TypeScript
filename: messages.uk.xlf
change_type: ModificationType.MODIFY
diff: @@ -342,14 +342,6 @@
         <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>

programming_language: TypeScript
filename: messages.vi.xlf
change_type: ModificationType.MODIFY
diff: @@ -389,14 +389,6 @@
         <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
             -->

programming_language: HTML
filename: messages.zh_CN.xlf
change_type: ModificationType.MODIFY
diff: @@ -349,14 +349,6 @@
         <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
             -->

programming_language: HTML
filename: messages.zh_Hant.xlf
change_type: ModificationType.MODIFY
diff: @@ -697,10 +697,6 @@
         <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>
@@ -721,10 +717,6 @@
         <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>

programming_language: TypeScript
filename: wizard.de.xlf
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,14 @@
         <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>

programming_language: HTML
filename: wizard.en.xlf
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,14 @@
         <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>

programming_language: HTML