Release 2.17 (#4836)

see https://github.com/kimai/kimai/pull/4836
This commit is contained in:
Kevin Papst
2024-05-19 17:42:03 +02:00
committed by GitHub
parent 5f7114e008
commit 0c445d1bc4
41 changed files with 318 additions and 307 deletions

View File

@@ -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'

280
composer.lock generated
View File

@@ -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",

View File

@@ -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']
# --------------------------------------------------------------------------------

View File

@@ -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"]

View File

@@ -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: ~

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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));

View File

@@ -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));

View File

@@ -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
*/

View File

@@ -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();

View File

@@ -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'])]

View File

@@ -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
}

View File

@@ -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 */

View File

@@ -71,7 +71,7 @@ class Tag
public function setName(?string $tagName): Tag
{
$this->name = $tagName;
$this->name = $tagName !== null ? trim($tagName) : $tagName;
return $this;
}

View File

@@ -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);

View File

@@ -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']);

View File

@@ -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'));
}

View File

@@ -45,29 +45,47 @@ final class TagArrayToStringTransformer implements DataTransformerInterface
*
* @see \Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
*
* @param string|null $value
* @param array<string>|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;
}
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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<string> $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) {

View File

@@ -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');

View File

@@ -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);

View File

@@ -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)) {

View File

@@ -30,7 +30,7 @@ class BaseQuery
/** @var array<string, string|int|null|bool|array<mixed>|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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}

View File

@@ -45,28 +45,24 @@
{% endblock %}
{% block report_form_layout %}
{{ form_start(form, {'attr': {'class': 'form-reporting', 'id': 'report-form'}}) }}
<div class="btn-list w-100">
{{ form_widget(form.month, {'label': false}) }}
{{ form_widget(form.customer, {'label': false, 'placeholder': 'please_choose'}) }}
<div class="dropdown">
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ icon('filter', true) }}
</button>
<ul class="dropdown-menu checkbox-menu">
{% for option in form.budgetType.children %}
<li class="dropdown-item">
{{ form_widget(option) }}
</li>
{% endfor %}
<li class="dropdown-divider"></li>
{{ form_widget(form.month, {'label': false}) }}
{{ form_widget(form.customer, {'label': false, 'placeholder': 'please_choose'}) }}
<div class="dropdown">
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ icon('filter', true) }}
</button>
<ul class="dropdown-menu checkbox-menu">
{% for option in form.budgetType.children %}
<li class="dropdown-item">
{{ form_widget(form.includeNoWork) }}
{{ form_widget(option) }}
</li>
</ul>
</div>
{% endfor %}
<li class="dropdown-divider"></li>
<li class="dropdown-item">
{{ form_widget(form.includeNoWork) }}
</li>
</ul>
</div>
{{ form_end(form) }}
{% endblock %}
{% block report %}

View File

@@ -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,

View File

@@ -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());
}
}

View File

@@ -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,
]);

View File

@@ -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'),
]);

View File

@@ -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,
]);

View File

@@ -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],

View File

@@ -72,7 +72,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$client->submit($form, [
'state' => 1,
'users' => [$user->getId()],
'pageSize' => 25,
'size' => 25,
'daterange' => $dateRange,
'customers' => [],
]);

View File

@@ -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,
]);

View File

@@ -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);

View File

@@ -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());