diff --git a/.gitignore b/.gitignore index 5a20c2c0..9dd9ded2 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/composer.json b/composer.json index d8f312b5..d924ff80 100644 --- a/composer.json +++ b/composer.json @@ -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": "*", diff --git a/composer.lock b/composer.lock index 2494b665..3aa6b4a6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "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", diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 468d9874..78baf712 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -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 diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index f4ab8981..30a2df2e 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -17,8 +17,6 @@ framework: cookie_httponly: true storage_factory_id: session.storage.factory.native - #esi: ~ - #fragments: ~ php_errors: log: true diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index cda11570..b50736cc 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -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 # -------------------------------------------------------------------------------- diff --git a/config/packages/security.yaml b/config/packages/security.yaml index c7b5e49c..3237af0a 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -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 diff --git a/config/routes.yaml b/config/routes.yaml index 27597722..af7c31f3 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -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: / diff --git a/config/services.yaml b/config/services.yaml index 3d027024..6637ab24 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -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 diff --git a/phpstan.neon b/phpstan.neon index 6f9b535c..c51d0183 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -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 diff --git a/src/API/ActionsController.php b/src/API/ActionsController.php index e7f6a4dc..e561d0e5 100644 --- a/src/API/ActionsController.php +++ b/src/API/ActionsController.php @@ -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')] diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php index f3555339..fe72917e 100644 --- a/src/API/ActivityController.php +++ b/src/API/ActivityController.php @@ -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; diff --git a/src/API/BaseApiController.php b/src/API/BaseApiController.php index 6cdfdee7..490904cc 100644 --- a/src/API/BaseApiController.php +++ b/src/API/BaseApiController.php @@ -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 - * @template TData of mixed + * @template TData of BaseQuery * @param class-string $type - * @param TData|null $data + * @param TData $data * @param array $options - * @return FormInterface + * @return FormInterface */ - 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') diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php index 24d96428..55cab78d 100644 --- a/src/API/CustomerController.php +++ b/src/API/CustomerController.php @@ -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; diff --git a/src/API/ProjectController.php b/src/API/ProjectController.php index 3e0ae95b..89e636cf 100644 --- a/src/API/ProjectController.php +++ b/src/API/ProjectController.php @@ -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; diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index aa81ab1c..8e2fa9e5 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -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); diff --git a/src/API/UserController.php b/src/API/UserController.php index 99246e40..a02fcf5e 100644 --- a/src/API/UserController.php +++ b/src/API/UserController.php @@ -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); } diff --git a/src/Activity/ActivityService.php b/src/Activity/ActivityService.php index dbb2e9d1..c82c41d1 100644 --- a/src/Activity/ActivityService.php +++ b/src/Activity/ActivityService.php @@ -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; /** diff --git a/src/Calendar/CalendarService.php b/src/Calendar/CalendarService.php index b0a5cede..9ffe6037 100644 --- a/src/Calendar/CalendarService.php +++ b/src/Calendar/CalendarService.php @@ -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 { diff --git a/src/Command/DemoteUserCommand.php b/src/Command/DemoteUserCommand.php index eb2c218b..99717e6b 100644 --- a/src/Command/DemoteUserCommand.php +++ b/src/Command/DemoteUserCommand.php @@ -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(); diff --git a/src/Command/InvoiceCreateCommand.php b/src/Command/InvoiceCreateCommand.php index 0066b5dc..a88bbe16 100644 --- a/src/Command/InvoiceCreateCommand.php +++ b/src/Command/InvoiceCreateCommand.php @@ -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 diff --git a/src/Command/UserLoginLinkCommand.php b/src/Command/UserLoginLinkCommand.php new file mode 100644 index 00000000..3da06baa --- /dev/null +++ b/src/Command/UserLoginLinkCommand.php @@ -0,0 +1,89 @@ +addArgument('email', InputArgument::REQUIRED, 'The email of the user'); + $this->addOption('password-reset', null, InputOption::VALUE_NONE, 'Whether the user needs to reset the password afterwards'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $email = $input->getArgument('email'); + if ($email === null || $email === '') { + $io->error('Need email to create login URL'); + + return Command::FAILURE; + } + + $user = $this->userRepository->findOneBy(['email' => $email]); + + if ($user === null) { + $io->error('Need username to create login URL'); + + return Command::FAILURE; + } + + if (!$user->isEnabled()) { + $io->error('User is not enabled'); + + return Command::FAILURE; + } + + if (!$user->isInternalUser()) { + $io->error('User does not use internal login'); + + return Command::FAILURE; + } + + $request = new Request(); + $request->setLocale($user->getLocale()); + $this->requestStack->push($request); + + $loginLinkDetails = $this->loginLink->createLoginLink($user, $request); + $loginLink = $loginLinkDetails->getUrl(); + + if ($input->getOption('password-reset') === true) { + $user->setPasswordRequestedAt(new \DateTime()); + $user->setRequiresPasswordReset(true); + $this->userRepository->saveUser($user); + } + + $output->writeln($loginLink); + + return Command::SUCCESS; + } +} diff --git a/src/Constants.php b/src/Constants.php index 9b6a49bb..2cfbf1b9 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -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'; /** diff --git a/src/Controller/AbstractController.php b/src/Controller/AbstractController.php index abdcb774..8f6c0b44 100644 --- a/src/Controller/AbstractController.php +++ b/src/Controller/AbstractController.php @@ -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 - * @template TData of mixed + * @template TData of array|object * @param class-string $type - * @param TData|null $data + * @param TData $data * @param array $options - * @return FormInterface + * @return FormInterface */ - protected function createFormWithName(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface + protected function createFormWithName(string $name, string $type, mixed $data, array $options = []): FormInterface { return $this->container->get('form.factory')->createNamed($name, $type, $data, $options); } diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php index b406d73f..ccf0fdca 100644 --- a/src/Controller/ActivityController.php +++ b/src/Controller/ActivityController.php @@ -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; diff --git a/src/Controller/ContractController.php b/src/Controller/ContractController.php index 1e5ceb99..fb44f1af 100644 --- a/src/Controller/ContractController.php +++ b/src/Controller/ContractController.php @@ -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 diff --git a/src/Controller/CustomerController.php b/src/Controller/CustomerController.php index c130442d..41f03040 100644 --- a/src/Controller/CustomerController.php +++ b/src/Controller/CustomerController.php @@ -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; diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index d58ced45..1b459f1b 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -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; diff --git a/src/Controller/HomepageController.php b/src/Controller/HomepageController.php index a45e664a..55120fca 100644 --- a/src/Controller/HomepageController.php +++ b/src/Controller/HomepageController.php @@ -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; diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index 9c85bce5..7ea0910b 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -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 diff --git a/src/Controller/PermissionController.php b/src/Controller/PermissionController.php index aa8c7a80..e7566710 100644 --- a/src/Controller/PermissionController.php +++ b/src/Controller/PermissionController.php @@ -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; diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index b288f3d1..7509e69d 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -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; diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 390f5736..a8546679 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -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; diff --git a/src/Controller/Reporting/ReportUsersMonthController.php b/src/Controller/Reporting/ReportUsersMonthController.php index 16f4e0d3..087e4231 100644 --- a/src/Controller/Reporting/ReportUsersMonthController.php +++ b/src/Controller/Reporting/ReportUsersMonthController.php @@ -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); diff --git a/src/Controller/Reporting/ReportUsersWeekController.php b/src/Controller/Reporting/ReportUsersWeekController.php index f4c7bcba..4b20c7d9 100644 --- a/src/Controller/Reporting/ReportUsersWeekController.php +++ b/src/Controller/Reporting/ReportUsersWeekController.php @@ -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); diff --git a/src/Controller/Reporting/ReportUsersYearController.php b/src/Controller/Reporting/ReportUsersYearController.php index 38671691..88e308a2 100644 --- a/src/Controller/Reporting/ReportUsersYearController.php +++ b/src/Controller/Reporting/ReportUsersYearController.php @@ -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); diff --git a/src/Controller/Reporting/UserMonthController.php b/src/Controller/Reporting/UserMonthController.php index 8799113a..93bf7755 100644 --- a/src/Controller/Reporting/UserMonthController.php +++ b/src/Controller/Reporting/UserMonthController.php @@ -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', ]; } } diff --git a/src/Controller/Reporting/UserWeekController.php b/src/Controller/Reporting/UserWeekController.php index 1fd5c91a..a0b2b7d3 100644 --- a/src/Controller/Reporting/UserWeekController.php +++ b/src/Controller/Reporting/UserWeekController.php @@ -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', ]; } } diff --git a/src/Controller/Reporting/UserYearController.php b/src/Controller/Reporting/UserYearController.php index cecee575..a4104af7 100644 --- a/src/Controller/Reporting/UserYearController.php +++ b/src/Controller/Reporting/UserYearController.php @@ -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', ]; } diff --git a/src/Controller/Security/LoginLinkController.php b/src/Controller/Security/LoginLinkController.php new file mode 100644 index 00000000..4d91644f --- /dev/null +++ b/src/Controller/Security/LoginLinkController.php @@ -0,0 +1,27 @@ +render('security/password-reset/check_email.html.twig', [ - 'tokenLifetime' => ceil($this->configuration->getPasswordResetRetryLifetime() / 3600), + 'tokenLifetime' => $this->configuration->getPasswordResetRetryLifetime(), ]); } diff --git a/src/Controller/Security/SelfRegistrationController.php b/src/Controller/Security/SelfRegistrationController.php index 6a78f268..073c4d70 100644 --- a/src/Controller/Security/SelfRegistrationController.php +++ b/src/Controller/Security/SelfRegistrationController.php @@ -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')] diff --git a/src/Controller/SystemConfigurationController.php b/src/Controller/SystemConfigurationController.php index 05c4c693..56c02405 100644 --- a/src/Controller/SystemConfigurationController.php +++ b/src/Controller/SystemConfigurationController.php @@ -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; diff --git a/src/Controller/TimesheetAbstractController.php b/src/Controller/TimesheetAbstractController.php index 0433e7cb..82b818ec 100644 --- a/src/Controller/TimesheetAbstractController.php +++ b/src/Controller/TimesheetAbstractController.php @@ -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); diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index bd0f5785..56c305ac 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -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; diff --git a/src/Controller/WizardController.php b/src/Controller/WizardController.php index 9e944ea3..d93277c5 100644 --- a/src/Controller/WizardController.php +++ b/src/Controller/WizardController.php @@ -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(), ]); diff --git a/src/Customer/CustomerService.php b/src/Customer/CustomerService.php index 167810bb..5ee56cd8 100644 --- a/src/Customer/CustomerService.php +++ b/src/Customer/CustomerService.php @@ -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 diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 6ea601e2..a87d7770 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -343,7 +343,7 @@ final class Configuration implements ConfigurationInterface ->defaultValue('{Y}/{cy,3}') ->end() ->booleanNode('upload_twig') - ->defaultTrue() + ->defaultFalse() ->end() ->end() ; diff --git a/src/Doctrine/ModifiedSubscriber.php b/src/Doctrine/ModifiedSubscriber.php new file mode 100644 index 00000000..e8218b51 --- /dev/null +++ b/src/Doctrine/ModifiedSubscriber.php @@ -0,0 +1,50 @@ +getObjectManager()->getUnitOfWork(); + $now = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if (!($entity instanceof Timesheet)) { + continue; + } + $entity->setModifiedAt($now); + } + + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if (!($entity instanceof Timesheet)) { + continue; + } + $entity->setModifiedAt($now); + } + } +} diff --git a/src/Doctrine/TimesheetSubscriber.php b/src/Doctrine/TimesheetSubscriber.php index 02a8c799..44eda77b 100644 --- a/src/Doctrine/TimesheetSubscriber.php +++ b/src/Doctrine/TimesheetSubscriber.php @@ -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 { /** diff --git a/src/Entity/Activity.php b/src/Entity/Activity.php index 1ac97f02..11451cf7 100644 --- a/src/Entity/Activity.php +++ b/src/Entity/Activity.php @@ -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 */ - #[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')] @@ -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'))] diff --git a/src/Entity/ActivityMeta.php b/src/Entity/ActivityMeta.php index f4f1561a..93c12943 100644 --- a/src/Entity/ActivityMeta.php +++ b/src/Entity/ActivityMeta.php @@ -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; diff --git a/src/Entity/ActivityRate.php b/src/Entity/ActivityRate.php index ff4de09b..59156066 100644 --- a/src/Entity/ActivityRate.php +++ b/src/Entity/ActivityRate.php @@ -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; diff --git a/src/Entity/Bookmark.php b/src/Entity/Bookmark.php index 25af24a7..82d3236d 100644 --- a/src/Entity/Bookmark.php +++ b/src/Entity/Bookmark.php @@ -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)] diff --git a/src/Entity/CommentTableTypeTrait.php b/src/Entity/CommentTableTypeTrait.php index 314e4b86..8a1b51d8 100644 --- a/src/Entity/CommentTableTypeTrait.php +++ b/src/Entity/CommentTableTypeTrait.php @@ -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)] diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 299c1042..efa866dd 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -156,7 +156,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget * * @var Collection */ - #[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')] @@ -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; diff --git a/src/Entity/CustomerComment.php b/src/Entity/CustomerComment.php index 012e9770..415e22b5 100644 --- a/src/Entity/CustomerComment.php +++ b/src/Entity/CustomerComment.php @@ -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; diff --git a/src/Entity/CustomerMeta.php b/src/Entity/CustomerMeta.php index f396e82a..828e1b97 100644 --- a/src/Entity/CustomerMeta.php +++ b/src/Entity/CustomerMeta.php @@ -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; diff --git a/src/Entity/CustomerRate.php b/src/Entity/CustomerRate.php index 4d747459..9ab493f4 100644 --- a/src/Entity/CustomerRate.php +++ b/src/Entity/CustomerRate.php @@ -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; diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index c5188b0e..30ae0f5e 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -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 */ - #[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')] diff --git a/src/Entity/InvoiceMeta.php b/src/Entity/InvoiceMeta.php index d41db1e9..01a837d9 100644 --- a/src/Entity/InvoiceMeta.php +++ b/src/Entity/InvoiceMeta.php @@ -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; diff --git a/src/Entity/Project.php b/src/Entity/Project.php index fbbdc1f6..c40fc64b 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -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 */ - #[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')] @@ -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'))] diff --git a/src/Entity/ProjectComment.php b/src/Entity/ProjectComment.php index ddba3cd2..b27e6a35 100644 --- a/src/Entity/ProjectComment.php +++ b/src/Entity/ProjectComment.php @@ -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; diff --git a/src/Entity/ProjectMeta.php b/src/Entity/ProjectMeta.php index 89e7fac2..5c5dcd19 100644 --- a/src/Entity/ProjectMeta.php +++ b/src/Entity/ProjectMeta.php @@ -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; diff --git a/src/Entity/ProjectRate.php b/src/Entity/ProjectRate.php index 7bdc8695..54095fe3 100644 --- a/src/Entity/ProjectRate.php +++ b/src/Entity/ProjectRate.php @@ -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; diff --git a/src/Entity/Rate.php b/src/Entity/Rate.php index 15e1c31b..cb7b71ce 100644 --- a/src/Entity/Rate.php +++ b/src/Entity/Rate.php @@ -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')] diff --git a/src/Entity/RolePermission.php b/src/Entity/RolePermission.php index 5c18e179..b1477acd 100644 --- a/src/Entity/RolePermission.php +++ b/src/Entity/RolePermission.php @@ -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)] diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php index febc8546..dc95b04f 100644 --- a/src/Entity/Tag.php +++ b/src/Entity/Tag.php @@ -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; diff --git a/src/Entity/Team.php b/src/Entity/Team.php index e6404fd9..222e9f98 100644 --- a/src/Entity/Team.php +++ b/src/Entity/Team.php @@ -47,7 +47,7 @@ class Team * * @var Collection */ - #[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 */ - #[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 */ - #[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 */ - #[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'))] diff --git a/src/Entity/TeamMember.php b/src/Entity/TeamMember.php index 946ceb14..2aed3f3e 100644 --- a/src/Entity/TeamMember.php +++ b/src/Entity/TeamMember.php @@ -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'])] diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index 253c1f3d..ba2feb65 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -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 */ - #[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')] @@ -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; diff --git a/src/Entity/TimesheetMeta.php b/src/Entity/TimesheetMeta.php index 4e116f64..380bb9a9 100644 --- a/src/Entity/TimesheetMeta.php +++ b/src/Entity/TimesheetMeta.php @@ -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; diff --git a/src/Entity/User.php b/src/Entity/User.php index 2f25f092..02ffd21d 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -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|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 */ - #[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__'); diff --git a/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php b/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php index df43b97d..75bc2ad9 100644 --- a/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php +++ b/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php @@ -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); } diff --git a/src/EventSubscriber/MenuBuilderSubscriber.php b/src/EventSubscriber/MenuBuilderSubscriber.php index 9876b402..a4f35239 100644 --- a/src/EventSubscriber/MenuBuilderSubscriber.php +++ b/src/EventSubscriber/MenuBuilderSubscriber.php @@ -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; /** diff --git a/src/EventSubscriber/UserPreferenceSubscriber.php b/src/EventSubscriber/UserPreferenceSubscriber.php index 3dd42d48..16504724 100644 --- a/src/EventSubscriber/UserPreferenceSubscriber.php +++ b/src/EventSubscriber/UserPreferenceSubscriber.php @@ -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; diff --git a/src/EventSubscriber/UserProfileSubscriber.php b/src/EventSubscriber/UserProfileSubscriber.php index 2de79f87..e1a07c16 100644 --- a/src/EventSubscriber/UserProfileSubscriber.php +++ b/src/EventSubscriber/UserProfileSubscriber.php @@ -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; diff --git a/src/EventSubscriber/WizardSubscriber.php b/src/EventSubscriber/WizardSubscriber.php index a2041782..a92b1484 100644 --- a/src/EventSubscriber/WizardSubscriber.php +++ b/src/EventSubscriber/WizardSubscriber.php @@ -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) { + if (!($user instanceof User)) { + return; + } + + if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) { + return; + } + + foreach (User::WIZARDS as $wizard) { + if (!$user->hasSeenWizard($wizard)) { + $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard])); + $event->setResponse($response); + return; } + } - if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) { - return; - } - - foreach (User::WIZARDS as $wizard) { - if (!$user->hasSeenWizard($wizard)) { - $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard])); - $event->setResponse($response); - - return; - } - } + if ($user->requiresPasswordReset()) { + $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => 'password'])); + $event->setResponse($response); } } } diff --git a/src/Export/Base/AbstractSpreadsheetRenderer.php b/src/Export/Base/AbstractSpreadsheetRenderer.php index fb8be045..c36dd918 100644 --- a/src/Export/Base/AbstractSpreadsheetRenderer.php +++ b/src/Export/Base/AbstractSpreadsheetRenderer.php @@ -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; diff --git a/src/Export/Base/HtmlRenderer.php b/src/Export/Base/HtmlRenderer.php index 69bde636..469587c0 100644 --- a/src/Export/Base/HtmlRenderer.php +++ b/src/Export/Base/HtmlRenderer.php @@ -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, diff --git a/src/Export/Base/PDFRenderer.php b/src/Export/Base/PDFRenderer.php index be8b3fe5..2f4def2e 100644 --- a/src/Export/Base/PDFRenderer.php +++ b/src/Export/Base/PDFRenderer.php @@ -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, diff --git a/src/Export/Renderer/HtmlRendererFactory.php b/src/Export/Renderer/HtmlRendererFactory.php index 3fc269ff..bd7ffa4f 100644 --- a/src/Export/Renderer/HtmlRendererFactory.php +++ b/src/Export/Renderer/HtmlRendererFactory.php @@ -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 diff --git a/src/Export/ServiceExport.php b/src/Export/ServiceExport.php index b7ada6f2..1cb640a8 100644 --- a/src/Export/ServiceExport.php +++ b/src/Export/ServiceExport.php @@ -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 { diff --git a/src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php b/src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php index 3f34c545..0644634c 100644 --- a/src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php +++ b/src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php @@ -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 diff --git a/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php b/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php index 4ec5f224..fae27f9c 100644 --- a/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php +++ b/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php @@ -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 diff --git a/src/Form/MultiUpdate/TimesheetMultiUpdate.php b/src/Form/MultiUpdate/TimesheetMultiUpdate.php index f7da39ac..dcf45f20 100644 --- a/src/Form/MultiUpdate/TimesheetMultiUpdate.php +++ b/src/Form/MultiUpdate/TimesheetMultiUpdate.php @@ -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, diff --git a/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php b/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php index a893c7b8..9f2af574 100644 --- a/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php +++ b/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php @@ -49,6 +49,8 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit * @var array */ 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; diff --git a/src/Form/Type/MenuChoiceType.php b/src/Form/Type/MenuChoiceType.php index 03701f1b..951b4c84 100644 --- a/src/Form/Type/MenuChoiceType.php +++ b/src/Form/Type/MenuChoiceType.php @@ -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; diff --git a/src/Form/Type/SkinType.php b/src/Form/Type/SkinType.php index aef0f899..2216ba45 100644 --- a/src/Form/Type/SkinType.php +++ b/src/Form/Type/SkinType.php @@ -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, diff --git a/src/Form/UserCreateType.php b/src/Form/UserCreateType.php index e2780d10..6c5a2dd5 100644 --- a/src/Form/UserCreateType.php +++ b/src/Form/UserCreateType.php @@ -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 diff --git a/src/Invoice/ServiceInvoice.php b/src/Invoice/ServiceInvoice.php index 01dd51bc..6cbe9d07 100644 --- a/src/Invoice/ServiceInvoice.php +++ b/src/Invoice/ServiceInvoice.php @@ -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. diff --git a/src/Kernel.php b/src/Kernel.php index b136ffbd..e769156c 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -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'); diff --git a/src/Project/ProjectService.php b/src/Project/ProjectService.php index e1c0c563..ecceaa7f 100644 --- a/src/Project/ProjectService.php +++ b/src/Project/ProjectService.php @@ -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; /** diff --git a/src/Project/ProjectStatisticService.php b/src/Project/ProjectStatisticService.php index 4cf6f782..09f06eb0 100644 --- a/src/Project/ProjectStatisticService.php +++ b/src/Project/ProjectStatisticService.php @@ -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 diff --git a/src/Reporting/ReportingService.php b/src/Reporting/ReportingService.php index 12ce5ae8..81cb03f8 100644 --- a/src/Reporting/ReportingService.php +++ b/src/Reporting/ReportingService.php @@ -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 diff --git a/src/Repository/TimesheetRepository.php b/src/Repository/TimesheetRepository.php index 6a99ece3..83b2b242 100644 --- a/src/Repository/TimesheetRepository.php +++ b/src/Repository/TimesheetRepository.php @@ -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(); diff --git a/src/Security/TwoFactorCondition.php b/src/Security/TwoFactorCondition.php index c9f6c5a3..ce501cdc 100644 --- a/src/Security/TwoFactorCondition.php +++ b/src/Security/TwoFactorCondition.php @@ -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; diff --git a/src/Timesheet/TimesheetService.php b/src/Timesheet/TimesheetService.php index 2f27c764..4856e3f1 100644 --- a/src/Timesheet/TimesheetService.php +++ b/src/Timesheet/TimesheetService.php @@ -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; diff --git a/src/Twig/Extensions.php b/src/Twig/Extensions.php index b3f75f6a..25e78b03 100644 --- a/src/Twig/Extensions.php +++ b/src/Twig/Extensions.php @@ -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 [ diff --git a/src/Twig/LocaleFormatExtensions.php b/src/Twig/LocaleFormatExtensions.php index 1b9736c7..9c044a47 100644 --- a/src/Twig/LocaleFormatExtensions.php +++ b/src/Twig/LocaleFormatExtensions.php @@ -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 [ diff --git a/src/Twig/PaginationExtension.php b/src/Twig/PaginationExtension.php index 605d7dd1..ef607d9b 100644 --- a/src/Twig/PaginationExtension.php +++ b/src/Twig/PaginationExtension.php @@ -27,9 +27,6 @@ final class PaginationExtension extends AbstractExtension { } - /** - * {@inheritdoc} - */ public function getFunctions(): array { return [ diff --git a/src/Twig/SecurityPolicy/ChainPolicy.php b/src/Twig/SecurityPolicy/ChainPolicy.php new file mode 100644 index 00000000..5cd30c00 --- /dev/null +++ b/src/Twig/SecurityPolicy/ChainPolicy.php @@ -0,0 +1,48 @@ + */ + private array $policies = []; + + public function __construct() + { + } + + public function addPolicy(SecurityPolicyInterface $policy): void + { + $this->policies[] = $policy; + } + + public function checkSecurity($tags, $filters, $functions): void + { + foreach ($this->policies as $policy) { + $policy->checkSecurity($tags, $filters, $functions); + } + } + + public function checkMethodAllowed($obj, $method): void + { + foreach ($this->policies as $policy) { + $policy->checkMethodAllowed($obj, $method); + } + } + + public function checkPropertyAllowed($obj, $property): void + { + foreach ($this->policies as $policy) { + $policy->checkPropertyAllowed($obj, $property); + } + } +} diff --git a/src/Twig/SecurityPolicy/DefaultPolicy.php b/src/Twig/SecurityPolicy/DefaultPolicy.php new file mode 100644 index 00000000..70a88221 --- /dev/null +++ b/src/Twig/SecurityPolicy/DefaultPolicy.php @@ -0,0 +1,30 @@ +policy = new ChainPolicy(); + $this->policy->addPolicy(new DefaultPolicy()); + } + + public function checkSecurity($tags, $filters, $functions): void + { + $this->policy->checkSecurity($tags, $filters, $functions); + } + + public function checkMethodAllowed($obj, $method): void + { + $this->policy->checkMethodAllowed($obj, $method); + } + + public function checkPropertyAllowed($obj, $property): void + { + $this->policy->checkPropertyAllowed($obj, $property); + } +} diff --git a/src/Twig/SecurityPolicy/ForbiddenPolicy.php b/src/Twig/SecurityPolicy/ForbiddenPolicy.php new file mode 100644 index 00000000..7c60522b --- /dev/null +++ b/src/Twig/SecurityPolicy/ForbiddenPolicy.php @@ -0,0 +1,109 @@ +> */ + private array $forbiddenMethods = []; + + /** + * @param array $forbiddenTags + * @param array $forbiddenFilters + * @param array> $forbiddenMethods + * @param array> $forbiddenProperties + * @param array $forbiddenFunctions + */ + public function __construct( + private array $forbiddenTags = [], + private array $forbiddenFilters = [], + array $forbiddenMethods = [], + private array $forbiddenProperties = [], + private array $forbiddenFunctions = [] + ) + { + $this->forbiddenMethods = []; + foreach ($forbiddenMethods as $class => $m) { + $this->forbiddenMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]); + } + } + + public function checkSecurity($tags, $filters, $functions): void + { + foreach ($tags as $tag) { + if (\in_array($tag, $this->forbiddenTags)) { + throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag); + } + } + + foreach ($filters as $filter) { + if (\in_array($filter, $this->forbiddenFilters)) { + throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter); + } + } + + foreach ($functions as $function) { + if (\in_array($function, $this->forbiddenFunctions)) { + throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function); + } + } + } + + public function checkMethodAllowed($obj, $method): void + { + if ($obj instanceof Template || $obj instanceof Markup) { + return; + } + + $forbidden = false; + $method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + foreach ($this->forbiddenMethods as $class => $methods) { + if ($obj instanceof $class) { + $forbidden = \in_array($method, $methods); + + break; + } + } + + if ($forbidden) { + $class = \get_class($obj); + throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); + } + } + + public function checkPropertyAllowed($obj, $property): void + { + $forbidden = false; + foreach ($this->forbiddenProperties as $class => $properties) { + if ($obj instanceof $class) { + $forbidden = \in_array($property, \is_array($properties) ? $properties : [$properties]); + + break; + } + } + + if ($forbidden) { + $class = \get_class($obj); + throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); + } + } +} diff --git a/src/Twig/SecurityPolicy/InvoicePolicy.php b/src/Twig/SecurityPolicy/InvoicePolicy.php new file mode 100644 index 00000000..655b021b --- /dev/null +++ b/src/Twig/SecurityPolicy/InvoicePolicy.php @@ -0,0 +1,86 @@ +policy = new ChainPolicy(); + $this->policy->addPolicy(new DefaultPolicy()); + $this->policy->addPolicy(new SecurityPolicy( + ['block', 'if', 'for', 'set', 'extends'], + [ + // Twig core filters + 'map', 'escape', 'trans', 'default', 'nl2br', 'trim', 'raw', + 'join', 'u', 'slice', 'date', 'month_name', 'first', 'country_name', + 'replace', 'length', 'number_format', 'split', + + // Kimai filters + 'md2html', 'desc2html', 'comment2html', 'comment1line', 'multiline_indent', 'nl2str', + 'date_short', 'duration', 'amount', 'money', 'duration_decimal', + ], + [ + PdfContext::class => ['setoption'], + InvoiceModel::class => ['toarray'], + ], + [], // properties + [ + // Twig core functions + 'cycle', 'asset', 'range', + + // Kimai functions + 'encore_entry_css_source', 'qr_code_data_uri', 'config', + ] + )); + } + + public function checkSecurity($tags, $filters, $functions): void + { + $this->policy->checkSecurity($tags, $filters, $functions); + } + + public function checkMethodAllowed($obj, $method): void + { + if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) { + return; + } + + $lm = strtolower($method); + + if (str_starts_with($lm, 'get') || str_starts_with($lm, 'is') || str_starts_with($lm, 'has')) { + return; + } + + if ($lm === '__tostring') { + return; + } + + $this->policy->checkMethodAllowed($obj, $method); + } + + public function checkPropertyAllowed($obj, $property): void + { + $this->policy->checkPropertyAllowed($obj, $property); + } +} diff --git a/src/Twig/TwigRendererTrait.php b/src/Twig/TwigRendererTrait.php index 1d1cadb0..a5ea5bd4 100644 --- a/src/Twig/TwigRendererTrait.php +++ b/src/Twig/TwigRendererTrait.php @@ -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) { diff --git a/src/User/LoginManager.php b/src/User/LoginManager.php index ef0e5e0a..128daba5 100644 --- a/src/User/LoginManager.php +++ b/src/User/LoginManager.php @@ -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 { diff --git a/src/User/UserService.php b/src/User/UserService.php index d7ad8f68..0ad868fd 100644 --- a/src/User/UserService.php +++ b/src/User/UserService.php @@ -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; diff --git a/src/Widget/Type/AbstractActiveUsers.php b/src/Widget/Type/AbstractActiveUsers.php index b7a12d0f..85173694 100644 --- a/src/Widget/Type/AbstractActiveUsers.php +++ b/src/Widget/Type/AbstractActiveUsers.php @@ -12,8 +12,8 @@ namespace App\Widget\Type; abstract class AbstractActiveUsers extends AbstractWidgetType { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { diff --git a/src/Widget/Type/AbstractAmountPeriod.php b/src/Widget/Type/AbstractAmountPeriod.php index 6072ec6d..0f9902dd 100644 --- a/src/Widget/Type/AbstractAmountPeriod.php +++ b/src/Widget/Type/AbstractAmountPeriod.php @@ -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 $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -41,7 +41,7 @@ abstract class AbstractAmountPeriod extends AbstractWidget } /** - * @param array $options + * @param array> $options * @return array */ protected function getRevenue(?\DateTimeInterface $begin, ?\DateTimeInterface $end, array $options = []): array diff --git a/src/Widget/Type/AbstractCounterDuration.php b/src/Widget/Type/AbstractCounterDuration.php index 7abcd195..023b228b 100644 --- a/src/Widget/Type/AbstractCounterDuration.php +++ b/src/Widget/Type/AbstractCounterDuration.php @@ -12,8 +12,8 @@ namespace App\Widget\Type; abstract class AbstractCounterDuration extends AbstractWidgetType { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { diff --git a/src/Widget/Type/AbstractCounterYear.php b/src/Widget/Type/AbstractCounterYear.php index c86ba561..efa8c580 100644 --- a/src/Widget/Type/AbstractCounterYear.php +++ b/src/Widget/Type/AbstractCounterYear.php @@ -21,7 +21,7 @@ abstract class AbstractCounterYear extends AbstractWidgetType } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { @@ -39,7 +39,7 @@ abstract class AbstractCounterYear extends AbstractWidgetType } /** - * @param array $options + * @param array> $options */ abstract protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed; diff --git a/src/Widget/Type/AbstractUserRevenuePeriod.php b/src/Widget/Type/AbstractUserRevenuePeriod.php index 4c693eca..a78a86c3 100644 --- a/src/Widget/Type/AbstractUserRevenuePeriod.php +++ b/src/Widget/Type/AbstractUserRevenuePeriod.php @@ -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 $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -46,7 +46,7 @@ abstract class AbstractUserRevenuePeriod extends AbstractWidget } /** - * @param array $options + * @param array> $options * @return array */ protected function getRevenue(?\DateTimeInterface $begin, ?\DateTimeInterface $end, array $options = []): array diff --git a/src/Widget/Type/AbstractWidget.php b/src/Widget/Type/AbstractWidget.php index 172133bf..bd005f25 100644 --- a/src/Widget/Type/AbstractWidget.php +++ b/src/Widget/Type/AbstractWidget.php @@ -68,8 +68,8 @@ abstract class AbstractWidget implements WidgetInterface } /** - * @param array $options - * @return array + * @param array> $options + * @return array> */ public function getOptions(array $options = []): array { diff --git a/src/Widget/Type/ActiveTimesheets.php b/src/Widget/Type/ActiveTimesheets.php index 36cd02ae..35a21a52 100644 --- a/src/Widget/Type/ActiveTimesheets.php +++ b/src/Widget/Type/ActiveTimesheets.php @@ -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 $options - * @return array + * @param array> $options + * @return array> */ 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 $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/ActiveUsersMonth.php b/src/Widget/Type/ActiveUsersMonth.php index 19afc82a..e6fdf6f3 100644 --- a/src/Widget/Type/ActiveUsersMonth.php +++ b/src/Widget/Type/ActiveUsersMonth.php @@ -20,8 +20,8 @@ final class ActiveUsersMonth extends AbstractActiveUsers } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -34,7 +34,7 @@ final class ActiveUsersMonth extends AbstractActiveUsers } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/ActiveUsersToday.php b/src/Widget/Type/ActiveUsersToday.php index ed68fffc..98897cfc 100644 --- a/src/Widget/Type/ActiveUsersToday.php +++ b/src/Widget/Type/ActiveUsersToday.php @@ -20,8 +20,8 @@ final class ActiveUsersToday extends AbstractActiveUsers } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -34,7 +34,7 @@ final class ActiveUsersToday extends AbstractActiveUsers } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/ActiveUsersTotal.php b/src/Widget/Type/ActiveUsersTotal.php index 9dfcb5fc..5a19c817 100644 --- a/src/Widget/Type/ActiveUsersTotal.php +++ b/src/Widget/Type/ActiveUsersTotal.php @@ -20,8 +20,8 @@ final class ActiveUsersTotal extends AbstractActiveUsers } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -34,7 +34,7 @@ final class ActiveUsersTotal extends AbstractActiveUsers } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/ActiveUsersWeek.php b/src/Widget/Type/ActiveUsersWeek.php index cc0ca9eb..1a653291 100644 --- a/src/Widget/Type/ActiveUsersWeek.php +++ b/src/Widget/Type/ActiveUsersWeek.php @@ -20,8 +20,8 @@ final class ActiveUsersWeek extends AbstractActiveUsers } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -34,7 +34,7 @@ final class ActiveUsersWeek extends AbstractActiveUsers } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/ActiveUsersYear.php b/src/Widget/Type/ActiveUsersYear.php index 76ed8b6a..13f59f5e 100644 --- a/src/Widget/Type/ActiveUsersYear.php +++ b/src/Widget/Type/ActiveUsersYear.php @@ -22,8 +22,8 @@ final class ActiveUsersYear extends AbstractCounterYear } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -34,7 +34,7 @@ final class ActiveUsersYear extends AbstractCounterYear } /** - * @param array $options + * @param array> $options */ protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed { diff --git a/src/Widget/Type/AmountMonth.php b/src/Widget/Type/AmountMonth.php index c8d2d123..c160c28b 100644 --- a/src/Widget/Type/AmountMonth.php +++ b/src/Widget/Type/AmountMonth.php @@ -14,8 +14,8 @@ use App\Widget\WidgetInterface; final class AmountMonth extends AbstractAmountPeriod { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -28,7 +28,7 @@ final class AmountMonth extends AbstractAmountPeriod } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/AmountToday.php b/src/Widget/Type/AmountToday.php index fdb0947c..7b09a8db 100644 --- a/src/Widget/Type/AmountToday.php +++ b/src/Widget/Type/AmountToday.php @@ -14,8 +14,8 @@ use App\Widget\WidgetInterface; final class AmountToday extends AbstractAmountPeriod { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -28,7 +28,7 @@ final class AmountToday extends AbstractAmountPeriod } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/AmountTotal.php b/src/Widget/Type/AmountTotal.php index d3bc6ec8..b3f3fd7e 100644 --- a/src/Widget/Type/AmountTotal.php +++ b/src/Widget/Type/AmountTotal.php @@ -14,8 +14,8 @@ use App\Widget\WidgetInterface; final class AmountTotal extends AbstractAmountPeriod { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -33,7 +33,7 @@ final class AmountTotal extends AbstractAmountPeriod } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/AmountWeek.php b/src/Widget/Type/AmountWeek.php index 38ecded3..6509b93a 100644 --- a/src/Widget/Type/AmountWeek.php +++ b/src/Widget/Type/AmountWeek.php @@ -14,8 +14,8 @@ use App\Widget\WidgetInterface; final class AmountWeek extends AbstractAmountPeriod { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -28,7 +28,7 @@ final class AmountWeek extends AbstractAmountPeriod } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/AmountYear.php b/src/Widget/Type/AmountYear.php index e18ebfb9..f5c164d7 100644 --- a/src/Widget/Type/AmountYear.php +++ b/src/Widget/Type/AmountYear.php @@ -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 $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -37,7 +37,7 @@ final class AmountYear extends AbstractCounterYear } /** - * @param array $options + * @param array> $options */ protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed { diff --git a/src/Widget/Type/DailyWorkingTimeChart.php b/src/Widget/Type/DailyWorkingTimeChart.php index 651a63db..7f15c50b 100644 --- a/src/Widget/Type/DailyWorkingTimeChart.php +++ b/src/Widget/Type/DailyWorkingTimeChart.php @@ -46,8 +46,8 @@ final class DailyWorkingTimeChart extends AbstractWidget } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -61,7 +61,7 @@ final class DailyWorkingTimeChart extends AbstractWidget } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/DurationMonth.php b/src/Widget/Type/DurationMonth.php index d9645f60..26a33214 100644 --- a/src/Widget/Type/DurationMonth.php +++ b/src/Widget/Type/DurationMonth.php @@ -20,8 +20,8 @@ final class DurationMonth extends AbstractCounterDuration } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -44,7 +44,7 @@ final class DurationMonth extends AbstractCounterDuration } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/DurationToday.php b/src/Widget/Type/DurationToday.php index 4ed11138..3923909b 100644 --- a/src/Widget/Type/DurationToday.php +++ b/src/Widget/Type/DurationToday.php @@ -20,8 +20,8 @@ final class DurationToday extends AbstractCounterDuration } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -44,7 +44,7 @@ final class DurationToday extends AbstractCounterDuration } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/DurationTotal.php b/src/Widget/Type/DurationTotal.php index 298f959b..b818d1a5 100644 --- a/src/Widget/Type/DurationTotal.php +++ b/src/Widget/Type/DurationTotal.php @@ -20,8 +20,8 @@ final class DurationTotal extends AbstractCounterDuration } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -44,7 +44,7 @@ final class DurationTotal extends AbstractCounterDuration } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/DurationWeek.php b/src/Widget/Type/DurationWeek.php index 1a98dba0..e7cd0d5a 100644 --- a/src/Widget/Type/DurationWeek.php +++ b/src/Widget/Type/DurationWeek.php @@ -20,8 +20,8 @@ final class DurationWeek extends AbstractCounterDuration } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -44,7 +44,7 @@ final class DurationWeek extends AbstractCounterDuration } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/DurationYear.php b/src/Widget/Type/DurationYear.php index 2e366f9b..7d0b8652 100644 --- a/src/Widget/Type/DurationYear.php +++ b/src/Widget/Type/DurationYear.php @@ -22,8 +22,8 @@ final class DurationYear extends AbstractCounterYear } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -34,7 +34,7 @@ final class DurationYear extends AbstractCounterYear } /** - * @param array $options + * @param array> $options */ protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed { diff --git a/src/Widget/Type/PaginatedWorkingTimeChart.php b/src/Widget/Type/PaginatedWorkingTimeChart.php index 9a65e45d..116921b1 100644 --- a/src/Widget/Type/PaginatedWorkingTimeChart.php +++ b/src/Widget/Type/PaginatedWorkingTimeChart.php @@ -48,8 +48,8 @@ final class PaginatedWorkingTimeChart extends AbstractWidget } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -87,7 +87,7 @@ final class PaginatedWorkingTimeChart extends AbstractWidget } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/TotalsActivity.php b/src/Widget/Type/TotalsActivity.php index c6adfa26..ab2fbe53 100644 --- a/src/Widget/Type/TotalsActivity.php +++ b/src/Widget/Type/TotalsActivity.php @@ -25,8 +25,8 @@ final class TotalsActivity extends AbstractWidget } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -38,7 +38,7 @@ final class TotalsActivity extends AbstractWidget } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/TotalsCustomer.php b/src/Widget/Type/TotalsCustomer.php index 78e3db72..a883b957 100644 --- a/src/Widget/Type/TotalsCustomer.php +++ b/src/Widget/Type/TotalsCustomer.php @@ -25,8 +25,8 @@ final class TotalsCustomer extends AbstractWidget } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -38,7 +38,7 @@ final class TotalsCustomer extends AbstractWidget } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/TotalsProject.php b/src/Widget/Type/TotalsProject.php index 74f81e3a..156c843f 100644 --- a/src/Widget/Type/TotalsProject.php +++ b/src/Widget/Type/TotalsProject.php @@ -25,8 +25,8 @@ final class TotalsProject extends AbstractWidget } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -38,7 +38,7 @@ final class TotalsProject extends AbstractWidget } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/TotalsUser.php b/src/Widget/Type/TotalsUser.php index e78af8ef..9cd11c6e 100644 --- a/src/Widget/Type/TotalsUser.php +++ b/src/Widget/Type/TotalsUser.php @@ -20,8 +20,8 @@ final class TotalsUser extends AbstractWidget } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -33,7 +33,7 @@ final class TotalsUser extends AbstractWidget } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserAmountMonth.php b/src/Widget/Type/UserAmountMonth.php index d7fc862b..e8bb6765 100644 --- a/src/Widget/Type/UserAmountMonth.php +++ b/src/Widget/Type/UserAmountMonth.php @@ -14,8 +14,8 @@ use App\Widget\WidgetInterface; final class UserAmountMonth extends AbstractUserRevenuePeriod { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -28,7 +28,7 @@ final class UserAmountMonth extends AbstractUserRevenuePeriod } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserAmountToday.php b/src/Widget/Type/UserAmountToday.php index 914b460c..afb1a2da 100644 --- a/src/Widget/Type/UserAmountToday.php +++ b/src/Widget/Type/UserAmountToday.php @@ -14,8 +14,8 @@ use App\Widget\WidgetInterface; final class UserAmountToday extends AbstractUserRevenuePeriod { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -28,7 +28,7 @@ final class UserAmountToday extends AbstractUserRevenuePeriod } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserAmountTotal.php b/src/Widget/Type/UserAmountTotal.php index 96601c36..ab3fbe80 100644 --- a/src/Widget/Type/UserAmountTotal.php +++ b/src/Widget/Type/UserAmountTotal.php @@ -14,8 +14,8 @@ use App\Widget\WidgetInterface; final class UserAmountTotal extends AbstractUserRevenuePeriod { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -28,7 +28,7 @@ final class UserAmountTotal extends AbstractUserRevenuePeriod } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserAmountWeek.php b/src/Widget/Type/UserAmountWeek.php index a9ab5991..6d7ff2b0 100644 --- a/src/Widget/Type/UserAmountWeek.php +++ b/src/Widget/Type/UserAmountWeek.php @@ -14,8 +14,8 @@ use App\Widget\WidgetInterface; final class UserAmountWeek extends AbstractUserRevenuePeriod { /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -28,7 +28,7 @@ final class UserAmountWeek extends AbstractUserRevenuePeriod } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserAmountYear.php b/src/Widget/Type/UserAmountYear.php index 00a1566b..0e59a3be 100644 --- a/src/Widget/Type/UserAmountYear.php +++ b/src/Widget/Type/UserAmountYear.php @@ -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 $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -57,7 +57,7 @@ final class UserAmountYear extends AbstractCounterYear } /** - * @param array $options + * @param array> $options */ protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed { diff --git a/src/Widget/Type/UserDurationMonth.php b/src/Widget/Type/UserDurationMonth.php index 89d7fae4..3b67243a 100644 --- a/src/Widget/Type/UserDurationMonth.php +++ b/src/Widget/Type/UserDurationMonth.php @@ -20,8 +20,8 @@ final class UserDurationMonth extends AbstractCounterDuration } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -39,7 +39,7 @@ final class UserDurationMonth extends AbstractCounterDuration } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserDurationToday.php b/src/Widget/Type/UserDurationToday.php index 2e6fcbf8..49cdb2c6 100644 --- a/src/Widget/Type/UserDurationToday.php +++ b/src/Widget/Type/UserDurationToday.php @@ -20,8 +20,8 @@ final class UserDurationToday extends AbstractCounterDuration } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -39,7 +39,7 @@ final class UserDurationToday extends AbstractCounterDuration } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserDurationTotal.php b/src/Widget/Type/UserDurationTotal.php index 9fe86e89..61a4c00d 100644 --- a/src/Widget/Type/UserDurationTotal.php +++ b/src/Widget/Type/UserDurationTotal.php @@ -20,8 +20,8 @@ final class UserDurationTotal extends AbstractCounterDuration } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -39,7 +39,7 @@ final class UserDurationTotal extends AbstractCounterDuration } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserDurationWeek.php b/src/Widget/Type/UserDurationWeek.php index 0b086836..e3170fc2 100644 --- a/src/Widget/Type/UserDurationWeek.php +++ b/src/Widget/Type/UserDurationWeek.php @@ -20,8 +20,8 @@ final class UserDurationWeek extends AbstractCounterDuration } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -39,7 +39,7 @@ final class UserDurationWeek extends AbstractCounterDuration } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserDurationYear.php b/src/Widget/Type/UserDurationYear.php index 207cb8a4..3305153e 100644 --- a/src/Widget/Type/UserDurationYear.php +++ b/src/Widget/Type/UserDurationYear.php @@ -32,8 +32,8 @@ final class UserDurationYear extends AbstractCounterYear } /** - * @param array $options - * @return array + * @param array> $options + @return array> */ public function getOptions(array $options = []): array { @@ -44,7 +44,7 @@ final class UserDurationYear extends AbstractCounterYear } /** - * @param array $options + * @param array> $options */ protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed { diff --git a/src/Widget/Type/UserTeamProjects.php b/src/Widget/Type/UserTeamProjects.php index 0409db7e..37b4177a 100644 --- a/src/Widget/Type/UserTeamProjects.php +++ b/src/Widget/Type/UserTeamProjects.php @@ -60,7 +60,7 @@ final class UserTeamProjects extends AbstractWidget } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/Type/UserTeams.php b/src/Widget/Type/UserTeams.php index 5a135c1d..8fa30503 100644 --- a/src/Widget/Type/UserTeams.php +++ b/src/Widget/Type/UserTeams.php @@ -53,7 +53,7 @@ final class UserTeams extends AbstractWidget } /** - * @param array $options + * @param array> $options */ public function getData(array $options = []): mixed { diff --git a/src/Widget/WidgetInterface.php b/src/Widget/WidgetInterface.php index ee1672fd..61e8fc74 100644 --- a/src/Widget/WidgetInterface.php +++ b/src/Widget/WidgetInterface.php @@ -71,7 +71,7 @@ interface WidgetInterface * make sure that the given $options will overwrite the internal option for * this one call. * - * @param array $options + * @param array> $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 $options - * @return array + * @param array> $options + * @return array> */ public function getOptions(array $options = []): array; diff --git a/src/WorkingTime/WorkingTimeService.php b/src/WorkingTime/WorkingTimeService.php index 49c71fcd..f3a7cbf4 100644 --- a/src/WorkingTime/WorkingTimeService.php +++ b/src/WorkingTime/WorkingTimeService.php @@ -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! diff --git a/symfony.lock b/symfony.lock index 16c3ce89..6aebb4f6 100644 --- a/symfony.lock +++ b/symfony.lock @@ -511,9 +511,6 @@ "ref": "179470cb6492db92dffee208cfdb436f175c93b4" } }, - "symfony/polyfill-ctype": { - "version": "v1.8.0" - }, "symfony/polyfill-intl-grapheme": { "version": "v1.13.1" }, diff --git a/templates/reporting/report_by_user.html.twig b/templates/reporting/report_by_user.html.twig index 9231ca08..e4edfc72 100644 --- a/templates/reporting/report_by_user.html.twig +++ b/templates/reporting/report_by_user.html.twig @@ -7,6 +7,7 @@ {{ column|date_weekday }} {% endblock %} + {% block upper_left %} {% endblock %} {% block column_classes_project -%} {% if column.date is weekend(user) %} weekend{% endif %}{% if column.date is today %} today{% endif %} {%- endblock %} diff --git a/templates/reporting/report_by_user_data.html.twig b/templates/reporting/report_by_user_data.html.twig index 0572ef73..1da8516c 100644 --- a/templates/reporting/report_by_user_data.html.twig +++ b/templates/reporting/report_by_user_data.html.twig @@ -17,10 +17,12 @@ - + {% for column in period.dateTimes %} - {% block period_name %}{% endblock %} + {% block period_name %} + + {% endblock %} {% set columns = columns + 1 %} {% set dateKey = column|report_date %} {% set totalsDuration = totalsDuration|merge({(dateKey): 0}) %} diff --git a/templates/reporting/report_by_user_layout.html.twig b/templates/reporting/report_by_user_layout.html.twig index c8e63b2d..49e4ce5c 100644 --- a/templates/reporting/report_by_user_layout.html.twig +++ b/templates/reporting/report_by_user_layout.html.twig @@ -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 %} diff --git a/templates/reporting/report_by_user_year.html.twig b/templates/reporting/report_by_user_year.html.twig index fb7c6728..35544a71 100644 --- a/templates/reporting/report_by_user_year.html.twig +++ b/templates/reporting/report_by_user_year.html.twig @@ -10,6 +10,7 @@ {% endblock %} + {% block upper_left %} {% endblock %} {% block column_classes_project %}{% endblock %} {% block column_classes_activity %}{% endblock %} {% block column_classes_total %}{% endblock %} diff --git a/templates/reporting/report_by_user_year_export.html.twig b/templates/reporting/report_by_user_year_export.html.twig new file mode 100644 index 00000000..8347bd7f --- /dev/null +++ b/templates/reporting/report_by_user_year_export.html.twig @@ -0,0 +1,8 @@ +{% embed 'reporting/report_by_user_data.html.twig' %} + {% block period_name %} + + {% endblock %} + {% block column_classes_project %}{% endblock %} + {% block column_classes_activity %}{% endblock %} + {% block column_classes_total %}{% endblock %} +{% endembed %} diff --git a/templates/security/password-reset/check_email.html.twig b/templates/security/password-reset/check_email.html.twig index 80bc47c6..e04313b7 100644 --- a/templates/security/password-reset/check_email.html.twig +++ b/templates/security/password-reset/check_email.html.twig @@ -3,7 +3,7 @@ {% block login_form %}

- {{ 'resetting.check_email'|trans({'%tokenLifetime%': tokenLifetime})|nl2br }} + {{ 'resetting.check_email'|trans({'%tokenLifetime%': tokenLifetime|duration})|nl2br }}

{% endblock %} diff --git a/templates/timesheet/multi-update.html.twig b/templates/timesheet/multi-update.html.twig index 28a062f1..3122dc04 100644 --- a/templates/timesheet/multi-update.html.twig +++ b/templates/timesheet/multi-update.html.twig @@ -22,7 +22,11 @@ -
+
+ {{ form_row(form.replaceDescription) }} + {{ form_row(form.description) }} +
+
{{ form_row(form.replaceTags) }} {{ form_row(form.tags) }}
diff --git a/templates/user/api-token.html.twig b/templates/user/api-token.html.twig index 9a4762b4..cbb72eab 100644 --- a/templates/user/api-token.html.twig +++ b/templates/user/api-token.html.twig @@ -13,7 +13,11 @@
- {{ 'api_password.intro'|trans }} +
+

{{ 'api_password.intro'|trans }}

+ +

{{ 'username'|trans }}: {{ user.userIdentifier }}

+
{{ icon('documentation', true) }} diff --git a/templates/widget/widget-counter.html.twig b/templates/widget/widget-counter.html.twig index 769bd3a3..2f43771d 100644 --- a/templates/widget/widget-counter.html.twig +++ b/templates/widget/widget-counter.html.twig @@ -7,12 +7,14 @@
diff --git a/templates/wizard/layout.html.twig b/templates/wizard/layout.html.twig index 14a9c0e1..2e9552f8 100644 --- a/templates/wizard/layout.html.twig +++ b/templates/wizard/layout.html.twig @@ -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 %} diff --git a/templates/wizard/password.html.twig b/templates/wizard/password.html.twig new file mode 100644 index 00000000..9d6ae750 --- /dev/null +++ b/templates/wizard/password.html.twig @@ -0,0 +1,14 @@ +{% extends 'wizard/layout.html.twig' %} + +{% block wizard_content %} +
+

{{ 'wizard.password.title'|trans({}, 'wizard') }}

+

{{ 'wizard.password.description'|trans({}, 'wizard') }}

+
+
{{ 'wizard.separator'|trans({}, 'wizard') }}
+
+
+ {{ form_rest(form) }} +
+
+{% endblock %} diff --git a/tests/API/APIControllerBaseTest.php b/tests/API/APIControllerBaseTest.php index 35a0e876..f225f535 100644 --- a/tests/API/APIControllerBaseTest.php +++ b/tests/API/APIControllerBaseTest.php @@ -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'], diff --git a/tests/API/UserControllerTest.php b/tests/API/UserControllerTest.php index cd16edd3..a679d1de 100644 --- a/tests/API/UserControllerTest.php +++ b/tests/API/UserControllerTest.php @@ -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']); diff --git a/tests/Command/UserLoginLinkCommandTest.php b/tests/Command/UserLoginLinkCommandTest.php new file mode 100644 index 00000000..541b0014 --- /dev/null +++ b/tests/Command/UserLoginLinkCommandTest.php @@ -0,0 +1,47 @@ +application = new Application($kernel); + + $loginLinkHandler = $this->createMock(LoginLinkHandlerInterface::class); + $userRepository = $this->createMock(UserRepository::class); + $requestStack = $this->createMock(RequestStack::class); + + $this->application->add(new UserLoginLinkCommand($loginLinkHandler, $userRepository, $requestStack)); + } + + public function testCommandName(): void + { + $application = $this->application; + + $command = $application->find('kimai:user:login-link'); + self::assertInstanceOf(UserLoginLinkCommand::class, $command); + } +} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 86193052..9d0614d1 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -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' => [ diff --git a/tests/Entity/TimesheetTest.php b/tests/Entity/TimesheetTest.php index 918173e6..5d91738c 100644 --- a/tests/Entity/TimesheetTest.php +++ b/tests/Entity/TimesheetTest.php @@ -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()); diff --git a/tests/Export/Renderer/PdfRendererTest.php b/tests/Export/Renderer/PdfRendererTest.php index 1491bc4f..d08acb40 100644 --- a/tests/Export/Renderer/PdfRendererTest.php +++ b/tests/Export/Renderer/PdfRendererTest.php @@ -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()); } } diff --git a/tests/Export/Timesheet/HtmlRendererTest.php b/tests/Export/Timesheet/HtmlRendererTest.php index 36ec4b70..5bf0d4b7 100644 --- a/tests/Export/Timesheet/HtmlRendererTest.php +++ b/tests/Export/Timesheet/HtmlRendererTest.php @@ -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'); diff --git a/tests/Export/Timesheet/PdfRendererTest.php b/tests/Export/Timesheet/PdfRendererTest.php index b866d4f1..e8ab60a8 100644 --- a/tests/Export/Timesheet/PdfRendererTest.php +++ b/tests/Export/Timesheet/PdfRendererTest.php @@ -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(); diff --git a/tests/Invoice/Renderer/PdfRendererTest.php b/tests/Invoice/Renderer/PdfRendererTest.php index c5282668..5fd0d8e0 100644 --- a/tests/Invoice/Renderer/PdfRendererTest.php +++ b/tests/Invoice/Renderer/PdfRendererTest.php @@ -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()); + } } } diff --git a/tests/Invoice/Renderer/TwigRendererTest.php b/tests/Invoice/Renderer/TwigRendererTest.php index bd076648..a24ad3ac 100644 --- a/tests/Invoice/Renderer/TwigRendererTest.php +++ b/tests/Invoice/Renderer/TwigRendererTest.php @@ -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()); + } + } } diff --git a/tests/Invoice/templates/default.pdf.twig b/tests/Invoice/templates/default.pdf.twig index 99fffdfe..2d575240 100644 --- a/tests/Invoice/templates/default.pdf.twig +++ b/tests/Invoice/templates/default.pdf.twig @@ -1,6 +1,5 @@ -{% 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 %} diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 24e0b8c5..e97254e5 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -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 diff --git a/translations/messages.ar.xlf b/translations/messages.ar.xlf index 6a55be80..0895dcec 100644 --- a/translations/messages.ar.xlf +++ b/translations/messages.ar.xlf @@ -756,10 +756,6 @@ searchTerm مصطلح البحث - - replaceTags - استبدل العلامات - color اللون @@ -804,10 +800,6 @@ set_as_default حفظ الإعداد ات كمفضلة البحث - - appendTags - إلحاق كلمات مفتاحية - stats.durationFinancialYear ساعات العمل هذه السنة المالية diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index cdd6bd18..4c761eab 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -886,13 +886,13 @@ profile.registration_date Registrován - - appendTags - Připojit štítky + + append + Připojit - - replaceTags - Nahradit štítky + + replace + Nahradit help.fixedRate diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index c23e8103..9d3de43d 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -349,13 +349,13 @@ color Farve - - replaceTags - Erstat etiketter + + replace + Erstat - - appendTags - Tilføj etiketter + + append + Tilføj @@ -1625,6 +1625,14 @@ supervisor Vorgesetzter + + force_password_change + Neues Passwort anfordern + + + force_password_change_help + Bei der nächsten Anmeldung muss der Benutzer ein neues Passwort hinterlegen + diff --git a/translations/messages.de_CH.xlf b/translations/messages.de_CH.xlf index c92de31f..efa66f3d 100644 --- a/translations/messages.de_CH.xlf +++ b/translations/messages.de_CH.xlf @@ -573,13 +573,13 @@ profile.title Benutzerprofil - - appendTags - Schlagworte hinzufügen + + append + Hinzufügen - - replaceTags - Schlagworte ersetzen + + replace + Ersetzen color diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 067f1eae..f4b5d901 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -441,13 +441,13 @@ color Χρώμα - - replaceTags - Αντικατάσταση ετικετών + + replace + Αντικατάσταση - - appendTags - Προσάρτηση ετικετών + + append + Προσάρτηση @@ -1625,6 +1625,14 @@ supervisor Supervisor + + force_password_change + Request new password + + + force_password_change_help + The next time the user logs in, they must enter a new password. + diff --git a/translations/messages.eo.xlf b/translations/messages.eo.xlf index 7498e71c..5faa560a 100644 --- a/translations/messages.eo.xlf +++ b/translations/messages.eo.xlf @@ -373,13 +373,13 @@ color Koloro - - replaceTags - Anstataŭigi etikedojn + + replace + Anstataŭigi - - appendTags - Postglui etikedojn + + append + Postglui @@ -624,10 +620,6 @@ fixedRate Fast sats - - appendTags - Legg til etiketter - calendar_initial_view Startvisning for kalender diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index cbff6117..36266906 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -942,14 +942,6 @@ lastLogin Laatste login - - appendTags - Voeg labels toe - - - replaceTags - Vervang labels - help.fixedRate Elke tijdsinvoer krijgt dezelfde prijs, ongeacht de duur ervan diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 49ade746..0cf5435a 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -361,13 +361,13 @@ color Kolor - - replaceTags - Zamień tagi + + replace + Zamień - - appendTags - Dodaj tagi + + append + Dodaj diff --git a/translations/messages.zh_CN.xlf b/translations/messages.zh_CN.xlf index c36a5482..ec85e51e 100644 --- a/translations/messages.zh_CN.xlf +++ b/translations/messages.zh_CN.xlf @@ -349,14 +349,6 @@ color 颜色 - - replaceTags - 替换标签 - - - appendTags - 附加标签 - diff --git a/translations/messages.zh_Hant.xlf b/translations/messages.zh_Hant.xlf index 851d4eb3..50f1747c 100644 --- a/translations/messages.zh_Hant.xlf +++ b/translations/messages.zh_Hant.xlf @@ -697,10 +697,6 @@ profile.roles 角色 - - replaceTags - 替換 tags - profile.about_me 關於我 @@ -721,10 +717,6 @@ color 色彩 - - appendTags - 添加 tags - profile.title 使用者個人資料 diff --git a/translations/wizard.de.xlf b/translations/wizard.de.xlf index 9f36a9d2..d7fcd991 100644 --- a/translations/wizard.de.xlf +++ b/translations/wizard.de.xlf @@ -22,6 +22,14 @@ wizard.profile.description Einige Grundeinstellungen um Ihnen die Arbeit mit Kimai zu erleichtern. + + wizard.password.title + Ihr Passwort + + + wizard.password.description + Bitte geben Sie ihr neues Passwort ein, das von nun an für die Anmeldung gelten soll. + wizard.done.title Herzlichen Glückwunsch diff --git a/translations/wizard.en.xlf b/translations/wizard.en.xlf index f8345847..792f7834 100644 --- a/translations/wizard.en.xlf +++ b/translations/wizard.en.xlf @@ -22,6 +22,14 @@ wizard.profile.description Some basic settings to make your work with Kimai easier. + + wizard.password.title + Your password + + + wizard.password.description + Please enter your new password, which will be used for logging in from now on. + wizard.done.title Congratulations
 {% block upper_left %}{{ user.displayName }}{% endblock %} {{ dataTypeTitle|trans }}{{ column|date_short }} {{ column|month_name }} {{ column|date_format('Y') }}