diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 7fcc28ff..e1e489ff 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -93,7 +93,6 @@ jobs: DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35 APP_ENV: test MAILER_URL: null://localhost - TEST_WITH_BUNDLES: 1 - name: Full test-suite with coverage if: matrix.php == '8.2' @@ -102,7 +101,6 @@ jobs: DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35 APP_ENV: dev MAILER_URL: null://localhost - TEST_WITH_BUNDLES: 1 - name: Upload code coverage if: matrix.php == '8.2' diff --git a/composer.lock b/composer.lock index fec3a39a..98834bb2 100644 --- a/composer.lock +++ b/composer.lock @@ -1387,16 +1387,16 @@ }, { "name": "doctrine/orm", - "version": "2.19.4", + "version": "2.19.5", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "b27489348658cd718d18005de37b94f7f8561467" + "reference": "94986af28452da42a46a4489d1c958a2e5d710e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/b27489348658cd718d18005de37b94f7f8561467", - "reference": "b27489348658cd718d18005de37b94f7f8561467", + "url": "https://api.github.com/repos/doctrine/orm/zipball/94986af28452da42a46a4489d1c958a2e5d710e5", + "reference": "94986af28452da42a46a4489d1c958a2e5d710e5", "shasum": "" }, "require": { @@ -1482,9 +1482,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.19.4" + "source": "https://github.com/doctrine/orm/tree/2.19.5" }, - "time": "2024-04-15T13:11:10+00:00" + "time": "2024-04-30T06:49:54+00:00" }, { "name": "doctrine/persistence", @@ -1586,23 +1586,26 @@ }, { "name": "doctrine/sql-formatter", - "version": "1.2.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "a321d114e0a18e6497f8a2cd6f890e000cc17ecc" + "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/a321d114e0a18e6497f8a2cd6f890e000cc17ecc", - "reference": "a321d114e0a18e6497f8a2cd6f890e000cc17ecc", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d1ac84aef745c69ea034929eb6d65a6908b675cc", + "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4" + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" }, "bin": [ "bin/sql-formatter" @@ -1632,9 +1635,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.2.0" + "source": "https://github.com/doctrine/sql-formatter/tree/1.4.0" }, - "time": "2023-08-16T21:49:04+00:00" + "time": "2024-05-08T08:12:09+00:00" }, { "name": "egulias/email-validator", @@ -3446,16 +3449,16 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v2.6.3", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "58c3f47f650c94ec05a151692652a868995d2938" + "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", - "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", + "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", "shasum": "" }, "require": { @@ -3509,7 +3512,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2022-06-14T06:56:20+00:00" + "time": "2024-05-08T12:18:48+00:00" }, { "name": "paragonie/random_compat", @@ -4005,16 +4008,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.28.0", + "version": "1.29.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb" + "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", - "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc", + "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc", "shasum": "" }, "require": { @@ -4046,9 +4049,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.28.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.0" }, - "time": "2024-04-03T18:51:33+00:00" + "time": "2024-05-06T12:04:23+00:00" }, { "name": "psr/cache", @@ -4304,20 +4307,20 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -4341,7 +4344,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -4353,9 +4356,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -5040,16 +5043,16 @@ }, { "name": "symfony/cache-contracts", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "2c9db6509a1b21dad229606897639d3284f54b2a" + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/2c9db6509a1b21dad229606897639d3284f54b2a", - "reference": "2c9db6509a1b21dad229606897639d3284f54b2a", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/df6a1a44c890faded49a5fca33c2d5c5fd3c2197", + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197", "shasum": "" }, "require": { @@ -5059,7 +5062,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -5096,7 +5099,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.4.2" + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.0" }, "funding": [ { @@ -5112,7 +5115,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/clock", @@ -5505,16 +5508,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { @@ -5523,7 +5526,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -5552,7 +5555,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -5568,7 +5571,7 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/doctrine-bridge", @@ -5909,16 +5912,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "4e64b49bf370ade88e567de29465762e316e4224" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/4e64b49bf370ade88e567de29465762e316e4224", - "reference": "4e64b49bf370ade88e567de29465762e316e4224", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { @@ -5928,7 +5931,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -5965,7 +5968,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.2" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" }, "funding": [ { @@ -5981,7 +5984,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/expression-language", @@ -6580,16 +6583,16 @@ }, { "name": "symfony/http-client-contracts", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "b6b5c876b3a4ed74460e2c5ac53bbce2f12e2a7e" + "reference": "20414d96f391677bf80078aa55baece78b82647d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/b6b5c876b3a4ed74460e2c5ac53bbce2f12e2a7e", - "reference": "b6b5c876b3a4ed74460e2c5ac53bbce2f12e2a7e", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", "shasum": "" }, "require": { @@ -6598,7 +6601,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -6638,7 +6641,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.4.2" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" }, "funding": [ { @@ -6654,7 +6657,7 @@ "type": "tidelift" } ], - "time": "2024-04-01T18:51:09+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/http-foundation", @@ -8706,21 +8709,22 @@ }, { "name": "symfony/service-contracts", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "11bbf19a0fb7b36345861e85c5768844c552906e" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/11bbf19a0fb7b36345861e85c5768844c552906e", - "reference": "11bbf19a0fb7b36345861e85c5768844c552906e", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -8728,7 +8732,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -8768,7 +8772,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.2" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -8784,7 +8788,7 @@ "type": "tidelift" } ], - "time": "2023-12-19T21:51:00+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/stopwatch", @@ -9031,16 +9035,16 @@ }, { "name": "symfony/translation-contracts", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b" + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/43810bdb2ddb5400e5c5e778e27b210a0ca83b6b", - "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", "shasum": "" }, "require": { @@ -9049,7 +9053,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -9089,7 +9093,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.4.2" + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0" }, "funding": [ { @@ -9105,7 +9109,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/twig-bridge", @@ -9756,16 +9760,16 @@ }, { "name": "twig/cssinliner-extra", - "version": "v3.9.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/cssinliner-extra.git", - "reference": "419e3e98431da91e8051ffdb447725d10935285d" + "reference": "10e88e9a887b646c58e3d670383208f15295dd22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/419e3e98431da91e8051ffdb447725d10935285d", - "reference": "419e3e98431da91e8051ffdb447725d10935285d", + "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/10e88e9a887b646c58e3d670383208f15295dd22", + "reference": "10e88e9a887b646c58e3d670383208f15295dd22", "shasum": "" }, "require": { @@ -9809,7 +9813,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.9.0" + "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.10.0" }, "funding": [ { @@ -9821,20 +9825,20 @@ "type": "tidelift" } ], - "time": "2023-12-10T19:34:32+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/extra-bundle", - "version": "v3.9.3", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "ef6869adf1fdab66f7e495771a7ba01496ffc0d5" + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/ef6869adf1fdab66f7e495771a7ba01496ffc0d5", - "reference": "ef6869adf1fdab66f7e495771a7ba01496ffc0d5", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/cdc6e23aeb7f4953c1039568c3439aab60c56454", + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454", "shasum": "" }, "require": { @@ -9883,7 +9887,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.9.3" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.10.0" }, "funding": [ { @@ -9895,20 +9899,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:24:21+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/inky-extra", - "version": "v3.9.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/inky-extra.git", - "reference": "95e489a56feaacff255deb1ffd9c7e2956985755" + "reference": "adfcc3b2becc09e909d30b813cde17351ac82958" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/95e489a56feaacff255deb1ffd9c7e2956985755", - "reference": "95e489a56feaacff255deb1ffd9c7e2956985755", + "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/adfcc3b2becc09e909d30b813cde17351ac82958", + "reference": "adfcc3b2becc09e909d30b813cde17351ac82958", "shasum": "" }, "require": { @@ -9953,7 +9957,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/inky-extra/tree/v3.9.0" + "source": "https://github.com/twigphp/inky-extra/tree/v3.10.0" }, "funding": [ { @@ -9965,26 +9969,26 @@ "type": "tidelift" } ], - "time": "2023-12-10T19:34:32+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/intl-extra", - "version": "v3.9.2", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/intl-extra.git", - "reference": "39865e5d13165016a8e7ab8cc648ad2f7aa4b639" + "reference": "693f6beb8ca91fc6323e01b3addf983812f65c93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/39865e5d13165016a8e7ab8cc648ad2f7aa4b639", - "reference": "39865e5d13165016a8e7ab8cc648ad2f7aa4b639", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/693f6beb8ca91fc6323e01b3addf983812f65c93", + "reference": "693f6beb8ca91fc6323e01b3addf983812f65c93", "shasum": "" }, "require": { "php": ">=7.2.5", "symfony/intl": "^5.4|^6.4|^7.0", - "twig/twig": "^3.9" + "twig/twig": "^3.10" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" @@ -10017,7 +10021,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/intl-extra/tree/v3.9.2" + "source": "https://github.com/twigphp/intl-extra/tree/v3.10.0" }, "funding": [ { @@ -10029,20 +10033,20 @@ "type": "tidelift" } ], - "time": "2024-04-17T12:41:53+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/string-extra", - "version": "v3.9.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/string-extra.git", - "reference": "5ff1c41366aa003d45f6e2707c5d698c1b37ff99" + "reference": "cd76ed8ae081bcd4fddf549e92e20c5df76c358a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/string-extra/zipball/5ff1c41366aa003d45f6e2707c5d698c1b37ff99", - "reference": "5ff1c41366aa003d45f6e2707c5d698c1b37ff99", + "url": "https://api.github.com/repos/twigphp/string-extra/zipball/cd76ed8ae081bcd4fddf549e92e20c5df76c358a", + "reference": "cd76ed8ae081bcd4fddf549e92e20c5df76c358a", "shasum": "" }, "require": { @@ -10084,7 +10088,7 @@ "unicode" ], "support": { - "source": "https://github.com/twigphp/string-extra/tree/v3.9.0" + "source": "https://github.com/twigphp/string-extra/tree/v3.10.0" }, "funding": [ { @@ -10096,20 +10100,20 @@ "type": "tidelift" } ], - "time": "2024-02-10T08:52:03+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/twig", - "version": "v3.9.3", + "version": "v3.10.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58" + "reference": "3af5ab2e52279e5e23dc192b1a26db3b8cffa4e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58", - "reference": "a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3af5ab2e52279e5e23dc192b1a26db3b8cffa4e7", + "reference": "3af5ab2e52279e5e23dc192b1a26db3b8cffa4e7", "shasum": "" }, "require": { @@ -10163,7 +10167,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.9.3" + "source": "https://github.com/twigphp/Twig/tree/v3.10.1" }, "funding": [ { @@ -10175,7 +10179,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T11:59:33+00:00" + "time": "2024-05-12T06:16:18+00:00" }, { "name": "webmozart/assert", @@ -10336,16 +10340,16 @@ }, { "name": "zircote/swagger-php", - "version": "4.9.0", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "b46a36d006f4db4d761995a5add1e7ab0386ed1d" + "reference": "256d42cb07ba1c2206d66bc7516ee3d3e3e9f0b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/b46a36d006f4db4d761995a5add1e7ab0386ed1d", - "reference": "b46a36d006f4db4d761995a5add1e7ab0386ed1d", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/256d42cb07ba1c2206d66bc7516ee3d3e3e9f0b2", + "reference": "256d42cb07ba1c2206d66bc7516ee3d3e3e9f0b2", "shasum": "" }, "require": { @@ -10411,9 +10415,9 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.9.0" + "source": "https://github.com/zircote/swagger-php/tree/4.9.2" }, - "time": "2024-04-18T22:32:11+00:00" + "time": "2024-05-02T21:36:00+00:00" } ], "packages-dev": [ @@ -10490,16 +10494,16 @@ }, { "name": "composer/xdebug-handler", - "version": "3.0.4", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { @@ -10536,7 +10540,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -10552,7 +10556,7 @@ "type": "tidelift" } ], - "time": "2024-03-26T18:29:49+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "dama/doctrine-test-bundle", @@ -10708,16 +10712,16 @@ }, { "name": "doctrine/doctrine-fixtures-bundle", - "version": "3.5.1", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", - "reference": "c808a0c85c38c8ee265cc8405b456c1d2b38567d" + "reference": "87f5d53708a3855aa018bf0a00d0d4b0ef58a956" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/c808a0c85c38c8ee265cc8405b456c1d2b38567d", - "reference": "c808a0c85c38c8ee265cc8405b456c1d2b38567d", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/87f5d53708a3855aa018bf0a00d0d4b0ef58a956", + "reference": "87f5d53708a3855aa018bf0a00d0d4b0ef58a956", "shasum": "" }, "require": { @@ -10746,7 +10750,7 @@ "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\FixturesBundle\\": "" + "Doctrine\\Bundle\\FixturesBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -10775,7 +10779,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.5.1" + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.6.0" }, "funding": [ { @@ -10791,7 +10795,7 @@ "type": "tidelift" } ], - "time": "2023-11-19T12:48:54+00:00" + "time": "2024-05-02T18:06:53+00:00" }, { "name": "fakerphp/faker", @@ -10858,16 +10862,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.54.0", + "version": "v3.56.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2aecbc8640d7906c38777b3dcab6f4ca79004d08" + "reference": "69c6168ae8bc96dc656c7f6c7271120a68ae5903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2aecbc8640d7906c38777b3dcab6f4ca79004d08", - "reference": "2aecbc8640d7906c38777b3dcab6f4ca79004d08", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/69c6168ae8bc96dc656c7f6c7271120a68ae5903", + "reference": "69c6168ae8bc96dc656c7f6c7271120a68ae5903", "shasum": "" }, "require": { @@ -10939,7 +10943,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.54.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.56.1" }, "funding": [ { @@ -10947,7 +10951,7 @@ "type": "github" } ], - "time": "2024-04-17T08:12:13+00:00" + "time": "2024-05-10T11:31:15+00:00" }, { "name": "masterminds/html5", diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index 96a66036..8013b2d3 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -118,9 +118,9 @@ kimai: # mapping a "role name" to an array of "permission names" roles: ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting'] - ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member'] - ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile'] - ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile'] + ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member','hours_other_profile'] + ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile'] + ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile'] # -------------------------------------------------------------------------------- diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index a7975752..062ec877 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -19,7 +19,7 @@ when@prod: type: console process_psr_3_messages: false channels: ["!event", "!doctrine", "!deprecation"] -# deactivated, because currently there are too many deprecations cause by gedmo and doctrine +# TODO reactivate once the many Doctrine deprecations are fixed # deprecation: # type: stream # channels: ["deprecation"] diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 4a304e89..39035224 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -72,8 +72,8 @@ security: login_link: check_route: link_login_check signature_properties: ['id'] - lifetime: 300 - max_uses: 1 + lifetime: 900 + max_uses: 3 access_decision_manager: # only grants access if there is no voter denying access @@ -108,7 +108,3 @@ when@test: cost: 4 # Lowest possible value for bcrypt time_cost: 3 # Lowest possible value for argon memory_cost: 10 # Lowest possible value for argon - - firewalls: - secured_area: - http_basic: ~ diff --git a/phpstan.neon b/phpstan.neon index af55029f..ed6df6a4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1158,11 +1158,6 @@ parameters: count: 1 path: src/Controller/TimesheetTeamController.php - - - message: "#^Parameter \\#2 \\$plainPassword of method Symfony\\\\Component\\\\PasswordHasher\\\\Hasher\\\\UserPasswordHasherInterface\\:\\:hashPassword\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/Controller/UserController.php - - message: "#^Parameter \\#2 \\$replace of method App\\\\User\\\\UserService\\:\\:deleteUser\\(\\) expects App\\\\Entity\\\\User\\|null, mixed given\\.$#" count: 1 diff --git a/src/API/Authentication/ApiRequestMatcher.php b/src/API/Authentication/ApiRequestMatcher.php index 2a275bda..02c39c2d 100644 --- a/src/API/Authentication/ApiRequestMatcher.php +++ b/src/API/Authentication/ApiRequestMatcher.php @@ -16,30 +16,19 @@ final class ApiRequestMatcher implements RequestMatcherInterface { public function matches(Request $request): bool { - // we do not want to handle URLs that + // we do not want to handle URLs that are not in the API scope if (!str_starts_with($request->getRequestUri(), '/api/')) { return false; } - // API documentation is only available to registered users + // API documentation is only available to registered and logged-in users if (str_starts_with($request->getRequestUri(), '/api/doc')) { return false; } - // let's use this firewall if a Bearer token is set in the header - // other cases like "bearer" are rejected earlier - if (($auth = $request->headers->get('Authorization')) !== null && str_starts_with($auth, 'Bearer ')) { - return true; - } - - // let's use this firewall if the deprecated username & token combination is available - if ($request->headers->has(TokenAuthenticator::HEADER_USERNAME) && - $request->headers->has(TokenAuthenticator::HEADER_TOKEN)) { - return true; - } - // checking for a previous session allows us to skip the API firewall and token access handler - // we simply re-use the existing session when doing API calls from the frontend + // we simply re-use the existing session when doing API calls from the frontend. + // it is not necessary to check headers. if there is no valid session, we should always use this firewall return !$request->hasPreviousSession(); } } diff --git a/src/Command/AbstractRoleCommand.php b/src/Command/AbstractRoleCommand.php index 8125827c..95b10e21 100644 --- a/src/Command/AbstractRoleCommand.php +++ b/src/Command/AbstractRoleCommand.php @@ -56,5 +56,5 @@ abstract class AbstractRoleCommand extends Command return Command::SUCCESS; } - abstract protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void; + abstract protected function executeRoleCommand(UserService $userService, SymfonyStyle $output, User $user, bool $super, $role): void; } diff --git a/src/Command/DemoteUserCommand.php b/src/Command/DemoteUserCommand.php index f51845d2..60054833 100644 --- a/src/Command/DemoteUserCommand.php +++ b/src/Command/DemoteUserCommand.php @@ -33,13 +33,13 @@ final class DemoteUserCommand extends AbstractRoleCommand ); } - protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void + protected function executeRoleCommand(UserService $userService, SymfonyStyle $output, User $user, bool $super, $role): void { $username = $user->getUserIdentifier(); if ($super) { if ($user->isSuperAdmin()) { $user->setSuperAdmin(false); - $manipulator->saveUser($user); + $userService->saveUser($user); $output->success(sprintf('Super administrator role has been removed from the user "%s".', $username)); } else { $output->warning(sprintf('User "%s" doesn\'t have the super administrator role.', $username)); @@ -47,7 +47,7 @@ final class DemoteUserCommand extends AbstractRoleCommand } else { if ($user->hasRole($role)) { $user->removeRole($role); - $manipulator->saveUser($user); + $userService->saveUser($user); $output->success(sprintf('Role "%s" has been removed from user "%s".', $role, $username)); } else { $output->warning(sprintf('User "%s" didn\'t have "%s" role.', $username, $role)); diff --git a/src/Command/PromoteUserCommand.php b/src/Command/PromoteUserCommand.php index f81ed966..8e162f0f 100644 --- a/src/Command/PromoteUserCommand.php +++ b/src/Command/PromoteUserCommand.php @@ -33,13 +33,13 @@ final class PromoteUserCommand extends AbstractRoleCommand ); } - protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void + protected function executeRoleCommand(UserService $userService, SymfonyStyle $output, User $user, bool $super, $role): void { $username = $user->getUserIdentifier(); if ($super) { if (!$user->isSuperAdmin()) { $user->setSuperAdmin(true); - $manipulator->saveUser($user); + $userService->saveUser($user); $output->success(sprintf('User "%s" has been promoted as a super administrator.', $username)); } else { $output->warning(sprintf('User "%s" does already have the super administrator role.', $username)); @@ -47,7 +47,7 @@ final class PromoteUserCommand extends AbstractRoleCommand } else { if (!$user->hasRole($role)) { $user->addRole($role); - $manipulator->saveUser($user); + $userService->saveUser($user); $output->success(sprintf('Role "%s" has been added to user "%s".', $role, $username)); } else { $output->warning(sprintf('User "%s" did already have "%s" role.', $username, $role)); diff --git a/src/Constants.php b/src/Constants.php index 3da8fd76..24368626 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ class Constants /** * The current release version */ - public const VERSION = '2.16.1'; + public const VERSION = '2.17.0'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 21601; + public const VERSION_ID = 21700; /** * The software name */ diff --git a/src/Controller/ContractController.php b/src/Controller/ContractController.php index e245adf1..533d1a96 100644 --- a/src/Controller/ContractController.php +++ b/src/Controller/ContractController.php @@ -31,7 +31,7 @@ final class ContractController extends AbstractController { $currentUser = $this->getUser(); $dateTimeFactory = $this->getDateTimeFactory($currentUser); - $canChangeUser = $this->isGranted('contract_other_profile'); + $canChangeUser = $this->isGranted('hours_other_profile'); $defaultDate = $dateTimeFactory->createStartOfYear(); $now = $dateTimeFactory->createDateTime(); diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 1c357424..0922c923 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -58,7 +58,12 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/admin/project')] final class ProjectController extends AbstractController { - public function __construct(private ProjectRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ProjectService $projectService) + public function __construct( + private readonly ProjectRepository $repository, + private readonly SystemConfiguration $configuration, + private readonly EventDispatcherInterface $dispatcher, + private readonly ProjectService $projectService + ) { } @@ -480,11 +485,17 @@ final class ProjectController extends AbstractController $csrfTokenManager->refreshToken('project.duplicate'); - $newProject = $projectDuplicationService->duplicate($project, $project->getName() . ' [COPY]'); + try { + $newProject = $projectDuplicationService->duplicate($project, $project->getName() . ' [COPY]'); + $this->flashSuccess('action.update.success'); - $this->flashSuccess('action.update.success'); + return $this->redirectToRoute('project_details', ['id' => $newProject->getId()]); + } catch (\Exception $ex) { + $this->logException($ex); + $this->flashError('action.update.error', 'Failed to copy project: ' . $ex->getMessage()); + } - return $this->redirectToRoute('project_details', ['id' => $newProject->getId()]); + return $this->redirectToRoute('admin_project'); } #[Route(path: '/{id}/delete', name: 'admin_project_delete', methods: ['GET', 'POST'])] diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index be1eb749..3c4a17d0 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -9,7 +9,6 @@ namespace App\Controller; -use App\Configuration\SystemConfiguration; use App\Entity\User; use App\Event\PrepareUserEvent; use App\Event\UserPreferenceDisplayEvent; @@ -29,7 +28,6 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -41,7 +39,10 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; #[IsGranted('view_user')] final class UserController extends AbstractController { - public function __construct(private UserPasswordHasherInterface $passwordHasher, private UserRepository $repository, private EventDispatcherInterface $dispatcher) + public function __construct( + private readonly UserRepository $repository, + private readonly EventDispatcherInterface $dispatcher + ) { } @@ -100,37 +101,23 @@ final class UserController extends AbstractController ]); } - private function createNewDefaultUser(SystemConfiguration $config): User - { - $user = new User(); - $user->setEnabled(true); - $user->setRoles([User::DEFAULT_ROLE]); - $user->setTimezone($config->getUserDefaultTimezone()); - $user->setLanguage($config->getUserDefaultLanguage()); - - return $user; - } - #[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])] #[IsGranted('create_user')] - public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository, EventDispatcherInterface $dispatcher): Response + public function createAction(Request $request, UserService $userService, EventDispatcherInterface $dispatcher): Response { - $user = $this->createNewDefaultUser($config); + $user = $userService->createNewUser(); $editForm = $this->getCreateUserForm($user); $editForm->handleRequest($request); if ($editForm->isSubmitted() && $editForm->isValid()) { - $password = $this->passwordHasher->hashPassword($user, $user->getPlainPassword()); - $user->setPassword($password); - - $userRepository->saveUser($user); + $userService->saveUser($user); $this->flashSuccess('action.update.success'); try { $event = new PrepareUserEvent($user, false); $dispatcher->dispatch($event); - $userRepository->saveUser($user); + $this->repository->saveUser($user); } catch (\Exception $ex) { // it should be no problem, if creating default user preferences fails } diff --git a/src/Entity/Project.php b/src/Entity/Project.php index b9d5b20f..b50afce0 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -481,6 +481,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget $this->addTeam($team); } + $this->number = null; $currentMeta = $this->meta; $this->meta = new ArrayCollection(); /** @var ProjectMeta $meta */ diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php index a2c7a757..b6107091 100644 --- a/src/Entity/Tag.php +++ b/src/Entity/Tag.php @@ -71,7 +71,7 @@ class Tag public function setName(?string $tagName): Tag { - $this->name = $tagName; + $this->name = $tagName !== null ? trim($tagName) : $tagName; return $this; } diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index 25cd6424..d3a0f213 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -154,6 +154,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt private ?string $description = null; #[ORM\Column(name: 'rate', type: 'float', nullable: false)] #[Assert\GreaterThanOrEqual(0)] + #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private float $rate = 0.00; @@ -224,8 +225,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt /** * Get entry id, returns null for new entities which were not persisted. - * - * @return int|null */ public function getId(): ?int { @@ -282,10 +281,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt return $this->end === null; } - /** - * @param DateTime $end - * @return Timesheet - */ public function setEnd(?DateTime $end): Timesheet { $this->end = $end; @@ -300,10 +295,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt return $this; } - /** - * @param int|null $duration - * @return Timesheet - */ public function setDuration(?int $duration): Timesheet { $this->duration = $duration; @@ -313,9 +304,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt /** * Do not rely on the results of this method for running records. - * - * @param bool $calculate - * @return int|null */ public function getDuration(bool $calculate = true): ?int { @@ -384,11 +372,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt return $this->description; } - /** - * @param float $rate - * @return Timesheet - */ - public function setRate($rate): Timesheet + public function setRate(float $rate): Timesheet { $this->rate = $rate; @@ -454,18 +438,11 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt ); } - /** - * @return bool - */ public function isExported(): bool { return $this->exported; } - /** - * @param bool $exported - * @return Timesheet - */ public function setExported(bool $exported): Timesheet { $this->exported = $exported; @@ -473,9 +450,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt return $this; } - /** - * @return string - */ public function getTimezone(): ?string { return $this->timezone; @@ -486,8 +460,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt * It is reserved for some very rare use-cases. * * @internal - * @param string $timezone - * @return Timesheet */ public function setTimezone(string $timezone): Timesheet { @@ -498,8 +470,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt /** * This method returns ALWAYS: "timesheet" - * - * @return string */ public function getType(): string { @@ -635,6 +605,10 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields { + // this needs to be done, otherwise doctrine will not see the item as changed + // and the calculators will not run + $this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + if (null === ($current = $this->getMetaField($meta->getName()))) { $meta->setEntity($this); $this->meta->add($meta); diff --git a/src/Event/PageActionsEvent.php b/src/Event/PageActionsEvent.php index b8ce69e4..70845fe2 100644 --- a/src/Event/PageActionsEvent.php +++ b/src/Event/PageActionsEvent.php @@ -141,6 +141,11 @@ class PageActionsEvent extends ThemeEvent $this->payload['actions'][$key] = null; } + public function addQuickImport(string $url): void + { + $this->addAction('import', ['url' => $url, 'class' => 'toolbar-action', 'title' => 'import', 'icon' => 'upload', 'translation_domain' => 'actions']); + } + public function addQuickExport(string $url): void { $this->addAction('download', ['url' => $url, 'class' => 'toolbar-action', 'title' => 'export']); diff --git a/src/EventSubscriber/MenuSubscriber.php b/src/EventSubscriber/MenuSubscriber.php index 3344c467..966f13e8 100644 --- a/src/EventSubscriber/MenuSubscriber.php +++ b/src/EventSubscriber/MenuSubscriber.php @@ -94,7 +94,7 @@ final class MenuSubscriber implements EventSubscriberInterface } $contract = new MenuItemModel('contract', 'work_contract', null, [], 'contract'); - if ($user->hasContractSettings() || $auth->isGranted('contract_other_profile')) { + if ($user->hasContractSettings() || $auth->isGranted('hours_other_profile')) { $contract->addChild(new MenuItemModel('contract_status', 'work_times', 'user_contract', [], 'work_times')); } diff --git a/src/Form/DataTransformer/TagArrayToStringTransformer.php b/src/Form/DataTransformer/TagArrayToStringTransformer.php index 1ec22a75..77576fe0 100644 --- a/src/Form/DataTransformer/TagArrayToStringTransformer.php +++ b/src/Form/DataTransformer/TagArrayToStringTransformer.php @@ -45,29 +45,47 @@ final class TagArrayToStringTransformer implements DataTransformerInterface * * @see \Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform() * - * @param string|null $value + * @param array|string|null $value * @return Tag[] * @throws TransformationFailedException */ - public function reverseTransform(mixed $value): mixed + public function reverseTransform(mixed $value): array { // check for empty tag list if ('' === $value || null === $value) { return []; } - $names = array_filter(array_unique(array_map('trim', explode(',', $value)))); + if (!\is_array($value)) { + $names = array_filter(array_unique(array_map('trim', explode(',', $value)))); + } else { + $names = $value; + } - // get the current tags and find the new ones that should be created - $tags = $this->tagRepository->findBy(['name' => $names]); - if ($this->create) { - // works, because of the implicit case: (string) $tag - $newNames = array_diff($names, $tags); + $tags = []; + foreach ($names as $tagName) { + if ($tagName === null || $tagName === '') { + continue; + } - foreach ($newNames as $name) { + $tagName = trim($tagName); + $tag = null; + + if (is_numeric($tagName)) { + $tag = $this->tagRepository->find($tagName); + } + + if ($tag === null) { + $tag = $this->tagRepository->findTagByName($tagName); + } + + // get the current tags and find the new ones that should be created + if ($this->create && $tag === null) { $tag = new Tag(); - $tag->setName(mb_substr($name, 0, 100)); + $tag->setName(mb_substr($tagName, 0, 100)); $this->tagRepository->saveTag($tag); + } + if ($tag !== null) { $tags[] = $tag; } } diff --git a/src/Form/FormTrait.php b/src/Form/FormTrait.php index 0ff9598b..b441e4eb 100644 --- a/src/Form/FormTrait.php +++ b/src/Form/FormTrait.php @@ -122,7 +122,7 @@ trait FormTrait return; } - $options['projects'] = $data['project']; + $options['projects'] = \is_string($data['project']) ? (int) $data['project'] : $data['project']; $event->getForm()->add('activity', ActivityType::class, $options); } diff --git a/src/Form/Toolbar/ToolbarFormTrait.php b/src/Form/Toolbar/ToolbarFormTrait.php index 6760f36a..6494fcb7 100644 --- a/src/Form/Toolbar/ToolbarFormTrait.php +++ b/src/Form/Toolbar/ToolbarFormTrait.php @@ -139,7 +139,7 @@ trait ToolbarFormTrait protected function addPageSizeChoice(FormBuilderInterface $builder): void { - $builder->add('pageSize', PageSizeType::class); + $builder->add('size', PageSizeType::class); } protected function addUserRoleChoice(FormBuilderInterface $builder): void diff --git a/src/Form/Type/TagsSelectType.php b/src/Form/Type/TagsSelectType.php index 455a6dcf..c4835e25 100644 --- a/src/Form/Type/TagsSelectType.php +++ b/src/Form/Type/TagsSelectType.php @@ -36,13 +36,15 @@ final class TagsSelectType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { - if (!$options['allow_create']) { - return; - } - - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) { /** @var array $tagIds */ $tagIds = $event->getData(); + + // this is mainly here, because the link from tags index page uses the non-array syntax + if (\is_string($tagIds) || \is_int($tagIds)) { + $tagIds = array_filter(array_unique(array_map('trim', explode(',', $tagIds)))); + } + if (!\is_array($tagIds)) { return; } @@ -59,13 +61,15 @@ final class TagsSelectType extends AbstractType $tag = $this->tagRepository->findTagByName($tagId); } - if ($tag === null) { + if ($options['allow_create'] && $tag === null) { $tag = new Tag(); - $tag->setName(mb_substr($tagId, 0, 100)); + $tag->setName($tagId); $this->tagRepository->saveTag($tag); } - $tags[] = $tag->getId(); + if ($tag !== null) { + $tags[] = $tag->getId(); + } } $event->setData($tags); @@ -79,6 +83,9 @@ final class TagsSelectType extends AbstractType 'class' => Tag::class, 'label' => 'tag', 'allow_create' => false, + 'choice_value' => function (Tag $tag) { + return $tag->getId(); + }, 'choice_attr' => function (Tag $tag) { $color = $tag->getColor(); if ($color === null) { diff --git a/src/Kernel.php b/src/Kernel.php index faaf271f..6e2b5d2e 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -60,7 +60,7 @@ class Kernel extends BaseKernel } } - if ($this->environment === 'test' && getenv('TEST_WITH_BUNDLES') === false) { + if ($this->environment === 'test') { return; } @@ -145,7 +145,7 @@ class Kernel extends BaseKernel $loader->load($file->getPathname()); } - if (is_file($confDir . '/packages/local.yaml')) { + if ($this->environment !== 'test' && is_file($confDir . '/packages/local.yaml')) { $loader->load($confDir . '/packages/local.yaml'); } $loader->load($confDir . '/services' . self::CONFIG_EXTS, 'glob'); diff --git a/src/Project/ProjectDuplicationService.php b/src/Project/ProjectDuplicationService.php index ee326628..75db8c28 100644 --- a/src/Project/ProjectDuplicationService.php +++ b/src/Project/ProjectDuplicationService.php @@ -9,9 +9,7 @@ namespace App\Project; -use App\Entity\ActivityRate; use App\Entity\Project; -use App\Entity\ProjectRate; use App\Repository\ActivityRateRepository; use App\Repository\ActivityRepository; use App\Repository\ProjectRateRepository; @@ -19,10 +17,10 @@ use App\Repository\ProjectRateRepository; final class ProjectDuplicationService { public function __construct( - private ProjectService $projectService, - private ActivityRepository $activityRepository, - private ProjectRateRepository $projectRateRepository, - private ActivityRateRepository $activityRateRepository + private readonly ProjectService $projectService, + private readonly ActivityRepository $activityRepository, + private readonly ProjectRateRepository $projectRateRepository, + private readonly ActivityRateRepository $activityRateRepository ) { } @@ -30,6 +28,7 @@ final class ProjectDuplicationService { $newProject = clone $project; $newProject->setName($newName); + $newProject->setNumber($this->projectService->calculateNextProjectNumber()); foreach ($project->getTeams() as $team) { $newProject->addTeam($team); @@ -49,7 +48,6 @@ final class ProjectDuplicationService $this->projectService->saveNewProject($newProject); foreach ($this->projectRateRepository->getRatesForProject($project) as $rate) { - /** @var ProjectRate $newRate */ $newRate = clone $rate; $newRate->setProject($newProject); $this->projectRateRepository->saveRate($newRate); @@ -68,7 +66,6 @@ final class ProjectDuplicationService $this->activityRepository->saveActivity($newActivity); foreach ($this->activityRateRepository->getRatesForActivity($activity) as $rate) { - /** @var ActivityRate $newRate */ $newRate = clone $rate; $newRate->setActivity($newActivity); $this->activityRateRepository->saveRate($newRate); diff --git a/src/Project/ProjectService.php b/src/Project/ProjectService.php index 68bc691c..246bb5cd 100644 --- a/src/Project/ProjectService.php +++ b/src/Project/ProjectService.php @@ -112,7 +112,7 @@ final class ProjectService return $this->repository->findOneBy(['number' => $number]); } - private function calculateNextProjectNumber(): ?string + public function calculateNextProjectNumber(): ?string { $format = $this->configuration->find('project.number_format'); if (empty($format) || !\is_string($format)) { diff --git a/src/Repository/Query/BaseQuery.php b/src/Repository/Query/BaseQuery.php index 4cf71930..b4b2be87 100644 --- a/src/Repository/Query/BaseQuery.php +++ b/src/Repository/Query/BaseQuery.php @@ -30,7 +30,7 @@ class BaseQuery /** @var array|DateRange> */ private array $defaults = [ 'page' => 1, - 'pageSize' => self::DEFAULT_PAGESIZE, + 'size' => self::DEFAULT_PAGESIZE, 'orderBy' => 'id', 'order' => self::ORDER_ASC, 'searchTerm' => null, @@ -137,6 +137,16 @@ class BaseQuery return $this; } + public function getSize(): int + { + return $this->getPageSize(); + } + + public function setSize(?int $size): void + { + $this->setPageSize($size); + } + public function getOrderBy(): string { return $this->orderBy; diff --git a/src/Security/SessionHandler.php b/src/Security/SessionHandler.php index 14ee0899..bebebe6d 100644 --- a/src/Security/SessionHandler.php +++ b/src/Security/SessionHandler.php @@ -53,6 +53,8 @@ final class SessionHandler extends PdoSessionHandler if (false === $limit->isAccepted()) { throw new BadRequestHttpException('Too many requests with invalid Session ID. Prediction attack?'); } + + usleep(250000); // slow down potential attacks } return $result; diff --git a/src/User/UserService.php b/src/User/UserService.php index f2028ce2..d4678dec 100644 --- a/src/User/UserService.php +++ b/src/User/UserService.php @@ -60,6 +60,7 @@ class UserService { $user = new User(); $user->setEnabled(true); + $user->setRoles([User::DEFAULT_ROLE]); $user->setTimezone($this->configuration->getUserDefaultTimezone()); $user->setLanguage($this->configuration->getUserDefaultLanguage()); $user->setPreferenceValue(UserPreference::SKIN, $this->configuration->getUserDefaultTheme()); @@ -92,6 +93,7 @@ class UserService $this->hashPassword($user); $this->hashApiToken($user); + $user->eraseCredentials(); $this->dispatcher->dispatch(new UserCreatePreEvent($user)); // @CloudRequired $this->repository->saveUser($user); @@ -120,6 +122,7 @@ class UserService $this->hashPassword($user); $this->hashApiToken($user); + $user->eraseCredentials(); $this->dispatcher->dispatch(new UserUpdatePreEvent($user)); $this->repository->saveUser($user); @@ -179,7 +182,6 @@ class UserService $password = $this->passwordHasher->hashPassword($user, $plain); $user->setPassword($password); - $user->eraseCredentials(); } private function hashApiToken(User $user): void @@ -192,7 +194,6 @@ class UserService $password = $this->passwordHasher->hashPassword($user, $plain); $user->setApiToken($password); - $user->eraseCredentials(); } public function deleteUser(User $delete, ?User $replace = null): void diff --git a/src/Voter/UserVoter.php b/src/Voter/UserVoter.php index d372ccfe..71427c5d 100644 --- a/src/Voter/UserVoter.php +++ b/src/Voter/UserVoter.php @@ -35,6 +35,7 @@ final class UserVoter extends Voter 'hourly-rate', 'view_team_member', 'contract', + 'hours', 'supervisor', ]; @@ -73,6 +74,10 @@ final class UserVoter extends Voter return $this->permissionManager->hasRolePermission($user, 'contract_other_profile'); } + if ($attribute === 'hours') { + return $this->permissionManager->hasRolePermission($user, 'hours_other_profile'); + } + if ($attribute === 'access_user') { return $user->canSeeUser($subject); } diff --git a/templates/reporting/project_daterange.html.twig b/templates/reporting/project_daterange.html.twig index 169f7517..781368c8 100644 --- a/templates/reporting/project_daterange.html.twig +++ b/templates/reporting/project_daterange.html.twig @@ -45,28 +45,24 @@ {% endblock %} {% block report_form_layout %} - {{ form_start(form, {'attr': {'class': 'form-reporting', 'id': 'report-form'}}) }} -
- {{ form_widget(form.month, {'label': false}) }} - {{ form_widget(form.customer, {'label': false, 'placeholder': 'please_choose'}) }} - - {{ form_end(form) }} {% endblock %} {% block report %} diff --git a/tests/Controller/ActivityControllerTest.php b/tests/Controller/ActivityControllerTest.php index 2b9f99dd..19da6fab 100644 --- a/tests/Controller/ActivityControllerTest.php +++ b/tests/Controller/ActivityControllerTest.php @@ -81,7 +81,7 @@ class ActivityControllerTest extends ControllerBaseTest $client->submit($form, [ 'searchTerm' => 'feature:timetracking foo', 'visibility' => 1, - 'pageSize' => 50, + 'size' => 50, 'customers' => [1], 'projects' => [1], 'page' => 1, @@ -125,7 +125,7 @@ class ActivityControllerTest extends ControllerBaseTest $client->submit($form, [ 'searchTerm' => 'feature:timetracking foo', 'visibility' => 1, - 'pageSize' => 50, + 'size' => 50, 'customers' => [1], 'projects' => [1], 'page' => 1, diff --git a/tests/Controller/ContractControllerTest.php b/tests/Controller/ContractControllerTest.php index 95e5c7c8..b395ed75 100644 --- a/tests/Controller/ContractControllerTest.php +++ b/tests/Controller/ContractControllerTest.php @@ -32,7 +32,20 @@ class ContractControllerTest extends ControllerBaseTest self::assertEquals(0, $node->count()); } - public function testAdminCanChangeUser(): void + public function testTeamleadCanChangeUser(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); + $this->assertAccessIsGranted($client, '/contract'); + $content = $client->getResponse()->getContent(); + self::assertNotFalse($content); + self::assertStringContainsString('No target hours have been configured', $content); + $node = $client->getCrawler()->filter('select#user'); + self::assertEquals(1, $node->count()); + $node = $client->getCrawler()->filter('a.alert-link'); + self::assertEquals(0, $node->count()); + } + + public function testAdminCanConfigureUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $this->assertAccessIsGranted($client, '/contract'); @@ -41,5 +54,7 @@ class ContractControllerTest extends ControllerBaseTest self::assertStringContainsString('No target hours have been configured', $content); $node = $client->getCrawler()->filter('select#user'); self::assertEquals(1, $node->count()); + $node = $client->getCrawler()->filter('a.alert-link'); + self::assertEquals(1, $node->count()); } } diff --git a/tests/Controller/CustomerControllerTest.php b/tests/Controller/CustomerControllerTest.php index a2a721f1..3d08fda4 100644 --- a/tests/Controller/CustomerControllerTest.php +++ b/tests/Controller/CustomerControllerTest.php @@ -86,7 +86,7 @@ class CustomerControllerTest extends ControllerBaseTest $client->submit($form, [ 'searchTerm' => 'feature:timetracking foo', 'visibility' => 1, - 'pageSize' => 50, + 'size' => 50, 'page' => 1, ]); @@ -119,7 +119,7 @@ class CustomerControllerTest extends ControllerBaseTest $client->submit($form, [ 'searchTerm' => 'feature:timetracking foo', 'visibility' => 1, - 'pageSize' => 50, + 'size' => 50, 'page' => 1, ]); diff --git a/tests/Controller/PermissionControllerTest.php b/tests/Controller/PermissionControllerTest.php index ffeb32d9..c4b89708 100644 --- a/tests/Controller/PermissionControllerTest.php +++ b/tests/Controller/PermissionControllerTest.php @@ -34,7 +34,7 @@ class PermissionControllerTest extends ControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/admin/permissions'); $this->assertHasDataTable($client); - $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 133); + $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 134); $this->assertPageActions($client, [ 'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'), ]); diff --git a/tests/Controller/ProjectControllerTest.php b/tests/Controller/ProjectControllerTest.php index 872aa4ff..2e2106c4 100644 --- a/tests/Controller/ProjectControllerTest.php +++ b/tests/Controller/ProjectControllerTest.php @@ -91,7 +91,7 @@ class ProjectControllerTest extends ControllerBaseTest 'searchTerm' => 'feature:timetracking foo', 'visibility' => 1, 'customers' => [1], - 'pageSize' => 50, + 'size' => 50, 'page' => 1, ]); @@ -134,7 +134,7 @@ class ProjectControllerTest extends ControllerBaseTest 'searchTerm' => 'feature:timetracking foo', 'visibility' => 1, 'customers' => [1], - 'pageSize' => 50, + 'size' => 50, 'page' => 1, ]); diff --git a/tests/Controller/TimesheetControllerTest.php b/tests/Controller/TimesheetControllerTest.php index 42d9744b..ccda19a2 100644 --- a/tests/Controller/TimesheetControllerTest.php +++ b/tests/Controller/TimesheetControllerTest.php @@ -71,7 +71,7 @@ class TimesheetControllerTest extends ControllerBaseTest $form = $client->getCrawler()->filter('form.searchform')->form(); $client->submit($form, [ 'state' => 1, - 'pageSize' => 25, + 'size' => 25, 'daterange' => $dateRange, 'customers' => [1], 'projects' => [1], diff --git a/tests/Controller/TimesheetTeamControllerTest.php b/tests/Controller/TimesheetTeamControllerTest.php index 749cbb85..01844d72 100644 --- a/tests/Controller/TimesheetTeamControllerTest.php +++ b/tests/Controller/TimesheetTeamControllerTest.php @@ -72,7 +72,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest $client->submit($form, [ 'state' => 1, 'users' => [$user->getId()], - 'pageSize' => 25, + 'size' => 25, 'daterange' => $dateRange, 'customers' => [], ]); diff --git a/tests/Controller/UserControllerTest.php b/tests/Controller/UserControllerTest.php index 902ea9e8..f0abdf6f 100644 --- a/tests/Controller/UserControllerTest.php +++ b/tests/Controller/UserControllerTest.php @@ -55,7 +55,7 @@ class UserControllerTest extends ControllerBaseTest 'searchTerm' => 'hourly_rate:35 tony', 'role' => 'ROLE_TEAMLEAD', 'visibility' => 1, - 'pageSize' => 50, + 'size' => 50, 'page' => 1, ]); @@ -89,7 +89,7 @@ class UserControllerTest extends ControllerBaseTest 'searchTerm' => 'hourly_rate:35 tony', 'role' => 'ROLE_TEAMLEAD', 'visibility' => 1, - 'pageSize' => 50, + 'size' => 50, 'page' => 1, ]); diff --git a/tests/Form/DataTransformer/TagArrayToStringTransformerTest.php b/tests/Form/DataTransformer/TagArrayToStringTransformerTest.php index b78b5440..4a3558ab 100644 --- a/tests/Form/DataTransformer/TagArrayToStringTransformerTest.php +++ b/tests/Form/DataTransformer/TagArrayToStringTransformerTest.php @@ -46,9 +46,9 @@ class TagArrayToStringTransformerTest extends TestCase ]; $repository = $this->getMockBuilder(TagRepository::class) - ->onlyMethods(['findBy', 'saveTag']) + ->onlyMethods(['findTagByName', 'saveTag']) ->disableOriginalConstructor()->getMock(); - $repository->expects($this->once())->method('findBy')->willReturn($results); + $repository->expects($this->exactly(3))->method('findTagByName')->willReturnOnConsecutiveCalls($results[0], $results[1]); $sut = new TagArrayToStringTransformer($repository, true); diff --git a/tests/Repository/Query/BaseQueryTest.php b/tests/Repository/Query/BaseQueryTest.php index 060f3ec4..08d2f639 100644 --- a/tests/Repository/Query/BaseQueryTest.php +++ b/tests/Repository/Query/BaseQueryTest.php @@ -48,7 +48,7 @@ class BaseQueryTest extends TestCase $sut->setPageSize(99); $sut->setSearchTerm(new SearchTerm('sdf')); - $this->resetByFormError($sut, ['order', 'orderBy', 'page', 'pageSize', 'searchTerm']); + $this->resetByFormError($sut, ['order', 'orderBy', 'page', 'size', 'searchTerm']); self::assertEquals(1, $sut->getPage()); self::assertEquals(50, $sut->getPageSize());