From b6c98f871d65a9db7177be8262ef5045ea657338 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Fri, 5 Apr 2024 12:38:21 +0200 Subject: [PATCH] Release 2.14 (#4710) - show "link has expired message" in password reset screen - added date objects as hydrator variables - for custom date formats in invoice templates - show meta-fields with null values (e.g. booleans with `false` where hidden) - fix permission check: allow to remove `view_own_timesheet` but still record times - prevent error 500 if customer country is empty - fix API 500 error if project does not exist when creating new timesheet - fix tags are not created in remote-search mode - do not check "export items" by default - fix daterange query, if user an request locale are different - added logging for invalid SAML responses (see various discussions) --- composer.lock | 159 +++++++++--------- phpstan.neon | 15 -- src/Constants.php | 4 +- src/Controller/AbstractController.php | 3 +- src/Controller/CustomerController.php | 2 +- .../Security/PasswordResetController.php | 2 + src/Controller/TimesheetController.php | 9 +- src/Controller/TimesheetTeamController.php | 9 +- .../TagArrayToStringTransformer.php | 37 ++-- src/Form/FormTrait.php | 15 +- src/Form/Toolbar/CustomerToolbarForm.php | 2 +- src/Form/Type/DateRangeType.php | 1 - src/Form/Type/TagsInputType.php | 13 +- src/Form/Type/TagsSelectType.php | 79 +++------ .../Hydrator/InvoiceModelDefaultHydrator.php | 14 +- src/Saml/SamlProvider.php | 13 +- src/Voter/ProjectVoter.php | 2 +- templates/macros/widgets.html.twig | 2 +- templates/user/contract.html.twig | 1 - tests/API/TimesheetControllerTest.php | 107 ++++++++++-- .../TagArrayToStringTransformerTest.php | 8 +- .../InvoiceModelDefaultHydratorTest.php | 6 + tests/Invoice/Renderer/DebugRendererTest.php | 6 + tests/Repository/Query/ExportQueryTest.php | 6 +- tests/Saml/SamlProviderTest.php | 5 +- tests/phpstan.neon | 5 - translations/messages.ar.xlf | 2 +- translations/messages.cs.xlf | 6 +- translations/messages.de.xlf | 8 +- translations/messages.en.xlf | 8 +- translations/messages.es.xlf | 6 +- translations/messages.fr.xlf | 6 +- translations/messages.he.xlf | 6 +- translations/messages.hr.xlf | 6 +- translations/messages.it.xlf | 6 +- translations/messages.nl.xlf | 6 +- translations/messages.pl.xlf | 2 +- translations/messages.pt.xlf | 6 +- translations/messages.pt_BR.xlf | 6 +- translations/messages.ru.xlf | 6 +- translations/messages.sk.xlf | 6 +- translations/messages.tr.xlf | 6 +- translations/messages.uk.xlf | 6 +- translations/messages.zh_CN.xlf | 6 +- translations/messages.zh_Hant.xlf | 6 +- 45 files changed, 318 insertions(+), 317 deletions(-) diff --git a/composer.lock b/composer.lock index 9298cf50..1aacf535 100644 --- a/composer.lock +++ b/composer.lock @@ -745,16 +745,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.11.3", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "492725310ae9a1b5b20d6ae09fb5ae6404616e68" + "reference": "5418e811a14724068e95e0ba43353b903ada530f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/492725310ae9a1b5b20d6ae09fb5ae6404616e68", - "reference": "492725310ae9a1b5b20d6ae09fb5ae6404616e68", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/5418e811a14724068e95e0ba43353b903ada530f", + "reference": "5418e811a14724068e95e0ba43353b903ada530f", "shasum": "" }, "require": { @@ -792,6 +792,7 @@ "symfony/property-info": "^5.4 || ^6.0 || ^7.0", "symfony/proxy-manager-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/security-bundle": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", "symfony/string": "^5.4 || ^6.0 || ^7.0", "symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/validator": "^5.4 || ^6.0 || ^7.0", @@ -809,7 +810,7 @@ "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\DoctrineBundle\\": "" + "Doctrine\\Bundle\\DoctrineBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -844,7 +845,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.11.3" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.12.0" }, "funding": [ { @@ -860,7 +861,7 @@ "type": "tidelift" } ], - "time": "2024-02-10T20:56:20+00:00" + "time": "2024-03-19T07:20:37+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -1386,16 +1387,16 @@ }, { "name": "doctrine/orm", - "version": "2.19.0", + "version": "2.19.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "a809a71aa6a233a6c82e68ebaaf8954adc4998dc" + "reference": "40a0964f06e142e173b783c45688e5f5efaed6aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/a809a71aa6a233a6c82e68ebaaf8954adc4998dc", - "reference": "a809a71aa6a233a6c82e68ebaaf8954adc4998dc", + "url": "https://api.github.com/repos/doctrine/orm/zipball/40a0964f06e142e173b783c45688e5f5efaed6aa", + "reference": "40a0964f06e142e173b783c45688e5f5efaed6aa", "shasum": "" }, "require": { @@ -1481,22 +1482,22 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.19.0" + "source": "https://github.com/doctrine/orm/tree/2.19.2" }, - "time": "2024-03-03T17:43:41+00:00" + "time": "2024-03-18T19:12:56+00:00" }, { "name": "doctrine/persistence", - "version": "3.3.1", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "b6fd1f126b13c1f7e7321f7338b14a19116b5de4" + "reference": "477da35bd0255e032826f440b94b3e37f2d56f42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/b6fd1f126b13c1f7e7321f7338b14a19116b5de4", - "reference": "b6fd1f126b13c1f7e7321f7338b14a19116b5de4", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/477da35bd0255e032826f440b94b3e37f2d56f42", + "reference": "477da35bd0255e032826f440b94b3e37f2d56f42", "shasum": "" }, "require": { @@ -1565,7 +1566,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/3.3.1" + "source": "https://github.com/doctrine/persistence/tree/3.3.2" }, "funding": [ { @@ -1581,7 +1582,7 @@ "type": "tidelift" } ], - "time": "2024-03-01T19:53:13+00:00" + "time": "2024-03-12T14:54:36+00:00" }, { "name": "doctrine/sql-formatter", @@ -2876,16 +2877,16 @@ }, { "name": "mpdf/mpdf", - "version": "v8.2.2", + "version": "v8.2.3", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "596a87b876d7793be7be060a8ac13424de120dd5" + "reference": "6f723a96becf989a831e38caf758d28364a69939" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/596a87b876d7793be7be060a8ac13424de120dd5", - "reference": "596a87b876d7793be7be060a8ac13424de120dd5", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/6f723a96becf989a831e38caf758d28364a69939", + "reference": "6f723a96becf989a831e38caf758d28364a69939", "shasum": "" }, "require": { @@ -2953,7 +2954,7 @@ "type": "custom" } ], - "time": "2023-11-07T13:52:14+00:00" + "time": "2024-03-11T12:55:53+00:00" }, { "name": "mpdf/psr-http-message-shim", @@ -3108,22 +3109,23 @@ }, { "name": "nelmio/api-doc-bundle", - "version": "v4.23.1", + "version": "v4.24.0", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "a15b5923602c669007ea53a1a87991e9e147daab" + "reference": "9fb5170b5f7a88d1aaade05e3eb399e697ac0941" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/a15b5923602c669007ea53a1a87991e9e147daab", - "reference": "a15b5923602c669007ea53a1a87991e9e147daab", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/9fb5170b5f7a88d1aaade05e3eb399e697ac0941", + "reference": "9fb5170b5f7a88d1aaade05e3eb399e697ac0941", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.2", - "phpdocumentor/reflection-docblock": "^3.1|^4.0|^5.0", + "php": ">=7.4", + "phpdocumentor/reflection-docblock": "^4.3.4|^5.0", + "phpdocumentor/type-resolver": "^1.8.2", "psr/cache": "^1.0|^2.0|^3.0", "psr/container": "^1.0|^2.0", "psr/log": "^1.0|^2.0|^3.0", @@ -3135,9 +3137,9 @@ "symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/http-kernel": "^5.4|^6.0|^7.0", "symfony/options-resolver": "^5.4|^6.0|^7.0", - "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4.10|^6.0|^7.0", "symfony/routing": "^5.4|^6.0|^7.0", - "zircote/swagger-php": "^4.2.15" + "zircote/swagger-php": "^4.6.1" }, "require-dev": { "api-platform/core": "^2.7.0|^3", @@ -3209,9 +3211,9 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.23.1" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.24.0" }, - "time": "2024-03-07T09:21:17+00:00" + "time": "2024-03-14T15:14:05+00:00" }, { "name": "nelmio/cors-bundle", @@ -10237,16 +10239,16 @@ }, { "name": "zircote/swagger-php", - "version": "4.8.4", + "version": "4.8.6", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "bdee7f5a9216ce103ba2c953c1c43c4a3e139e4c" + "reference": "fe45a0e98a156999ddafc31c9ad333fdcadbcb2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/bdee7f5a9216ce103ba2c953c1c43c4a3e139e4c", - "reference": "bdee7f5a9216ce103ba2c953c1c43c4a3e139e4c", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/fe45a0e98a156999ddafc31c9ad333fdcadbcb2d", + "reference": "fe45a0e98a156999ddafc31c9ad333fdcadbcb2d", "shasum": "" }, "require": { @@ -10312,24 +10314,24 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.8.4" + "source": "https://github.com/zircote/swagger-php/tree/4.8.6" }, - "time": "2024-02-04T21:16:47+00:00" + "time": "2024-03-15T01:13:55+00:00" } ], "packages-dev": [ { "name": "composer/pcre", - "version": "3.1.2", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace" + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/4775f35b2d70865807c89d32c8e7385b86eb0ace", - "reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", "shasum": "" }, "require": { @@ -10371,7 +10373,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.2" + "source": "https://github.com/composer/pcre/tree/3.1.3" }, "funding": [ { @@ -10387,7 +10389,7 @@ "type": "tidelift" } ], - "time": "2024-03-07T15:38:35+00:00" + "time": "2024-03-19T10:26:25+00:00" }, { "name": "composer/xdebug-handler", @@ -10759,16 +10761,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.51.0", + "version": "v3.52.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "127fa74f010da99053e3f5b62672615b72dd6efd" + "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/127fa74f010da99053e3f5b62672615b72dd6efd", - "reference": "127fa74f010da99053e3f5b62672615b72dd6efd", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/6e77207f0d851862ceeb6da63e6e22c01b1587bc", + "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc", "shasum": "" }, "require": { @@ -10839,7 +10841,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.51.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.52.1" }, "funding": [ { @@ -10847,7 +10849,7 @@ "type": "github" } ], - "time": "2024-02-28T19:50:06+00:00" + "time": "2024-03-19T21:02:43+00:00" }, { "name": "masterminds/html5", @@ -11094,16 +11096,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.60", + "version": "1.10.63", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe" + "reference": "ad12836d9ca227301f5fb9960979574ed8628339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/95dcea7d6c628a3f2f56d091d8a0219485a86bbe", - "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ad12836d9ca227301f5fb9960979574ed8628339", + "reference": "ad12836d9ca227301f5fb9960979574ed8628339", "shasum": "" }, "require": { @@ -11152,25 +11154,25 @@ "type": "tidelift" } ], - "time": "2024-03-07T13:30:19+00:00" + "time": "2024-03-18T16:53:53+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.3.62", + "version": "1.3.63", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "f3abbd8e93e12fed8091be3aeec216b06bed0950" + "reference": "6ccde2b243e30ba47f7cd29f87fb752263bcb25c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/f3abbd8e93e12fed8091be3aeec216b06bed0950", - "reference": "f3abbd8e93e12fed8091be3aeec216b06bed0950", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/6ccde2b243e30ba47f7cd29f87fb752263bcb25c", + "reference": "6ccde2b243e30ba47f7cd29f87fb752263bcb25c", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.48" + "phpstan/phpstan": "^1.10.63" }, "conflict": { "doctrine/collections": "<1.0", @@ -11222,9 +11224,9 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.62" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.63" }, - "time": "2024-02-12T11:52:17+00:00" + "time": "2024-03-18T18:05:58+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -11329,22 +11331,22 @@ }, { "name": "phpstan/phpstan-symfony", - "version": "1.3.8", + "version": "1.3.9", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "d8a0bc03a68d95288b6471c37d435647fbdaff1a" + "reference": "a32bc86da24495025d7aafd1ba62444d4a364a98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/d8a0bc03a68d95288b6471c37d435647fbdaff1a", - "reference": "d8a0bc03a68d95288b6471c37d435647fbdaff1a", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/a32bc86da24495025d7aafd1ba62444d4a364a98", + "reference": "a32bc86da24495025d7aafd1ba62444d4a364a98", "shasum": "" }, "require": { "ext-simplexml": "*", "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.36" + "phpstan/phpstan": "^1.10.62" }, "conflict": { "symfony/framework-bundle": "<3.0" @@ -11395,9 +11397,9 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.8" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.9" }, - "time": "2024-03-05T16:33:08+00:00" + "time": "2024-03-16T16:50:20+00:00" }, { "name": "phpunit/php-code-coverage", @@ -12622,16 +12624,16 @@ }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -12643,7 +12645,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -12664,8 +12666,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -12673,7 +12674,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", diff --git a/phpstan.neon b/phpstan.neon index 8c1c9e9a..28451043 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2202,21 +2202,6 @@ parameters: count: 1 path: src/Form/DataTransformer/StringToArrayTransformer.php - - - message: "#^Class App\\\\Form\\\\DataTransformer\\\\TagArrayToStringTransformer implements generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#" - count: 1 - path: src/Form/DataTransformer/TagArrayToStringTransformer.php - - - - message: "#^Parameter \\#1 \\$stringOfTags \\(string\\|null\\) of method App\\\\Form\\\\DataTransformer\\\\TagArrayToStringTransformer\\:\\:reverseTransform\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Form\\\\DataTransformerInterface\\\\:\\:reverseTransform\\(\\)$#" - count: 1 - path: src/Form/DataTransformer/TagArrayToStringTransformer.php - - - - message: "#^Parameter \\#1 \\$tags \\(array\\\\|null\\) of method App\\\\Form\\\\DataTransformer\\\\TagArrayToStringTransformer\\:\\:transform\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Form\\\\DataTransformerInterface\\\\:\\:transform\\(\\)$#" - count: 1 - path: src/Form/DataTransformer/TagArrayToStringTransformer.php - - message: "#^Method App\\\\Form\\\\Extension\\\\DocumentationLinkExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 diff --git a/src/Constants.php b/src/Constants.php index ddce22e2..8338735d 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ class Constants /** * The current release version */ - public const VERSION = '2.13.0'; + public const VERSION = '2.14.0'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 21300; + public const VERSION_ID = 21400; /** * The software name */ diff --git a/src/Controller/AbstractController.php b/src/Controller/AbstractController.php index c88d7986..2ccbdc26 100644 --- a/src/Controller/AbstractController.php +++ b/src/Controller/AbstractController.php @@ -255,6 +255,8 @@ abstract class AbstractController extends BaseAbstractController implements Serv } /** + * Use "performSearch=1" to skip loading session searches. + * * @param array $filterParams parameter names, which should not be saved (neither session, nor database) * @throws \Exception */ @@ -298,7 +300,6 @@ abstract class AbstractController extends BaseAbstractController implements Serv } $searchName = $this->getSearchName($data); - /** @var BookmarkRepository $bookmarkRepo */ $bookmarkRepo = $this->getBookmark(); $bookmark = $bookmarkRepo->getSearchDefaultOptions($this->getUser(), $searchName); diff --git a/src/Controller/CustomerController.php b/src/Controller/CustomerController.php index 456bd50b..6bacd4f3 100644 --- a/src/Controller/CustomerController.php +++ b/src/Controller/CustomerController.php @@ -325,7 +325,7 @@ final class CustomerController extends AbstractController $rates = $rateRepository->getRatesForCustomer($customer); } - if (null !== $customer->getTimezone()) { + if ($customer->getTimezone() !== null && $customer->getTimezone() !== '') { $timezone = new \DateTimeZone($customer->getTimezone()); } diff --git a/src/Controller/Security/PasswordResetController.php b/src/Controller/Security/PasswordResetController.php index 2da89feb..e03cd5a2 100644 --- a/src/Controller/Security/PasswordResetController.php +++ b/src/Controller/Security/PasswordResetController.php @@ -132,6 +132,8 @@ final class PasswordResetController extends AbstractController } if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetTokenLifetime())) { + $this->flashWarning('This link has already expired'); + return $this->redirectToRoute('resetting_request'); } diff --git a/src/Controller/TimesheetController.php b/src/Controller/TimesheetController.php index 63259388..e087eb91 100644 --- a/src/Controller/TimesheetController.php +++ b/src/Controller/TimesheetController.php @@ -19,8 +19,15 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; +/** + * No permission check on controller level, only for single routes. + * + * There was "view_own_timesheet" here once, but it is a bug, as some companies (rarely, but existing) want their + * employees to enter time, but not to see it afterward. + * + * It is legit to only own "create_own_timesheet" without "view_own_timesheet". + */ #[Route(path: '/timesheet')] -#[IsGranted('view_own_timesheet')] final class TimesheetController extends TimesheetAbstractController { #[Route(path: '/', defaults: ['page' => 1], name: 'timesheet', methods: ['GET'])] diff --git a/src/Controller/TimesheetTeamController.php b/src/Controller/TimesheetTeamController.php index f06e57b4..309b1186 100644 --- a/src/Controller/TimesheetTeamController.php +++ b/src/Controller/TimesheetTeamController.php @@ -27,8 +27,15 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; +/** + * No permission check on controller level, only for single routes. + * + * There was "view_other_timesheet" here once, but it is a bug. + * Some companies (rarely, but existing) want their employees to enter time, but not to see it afterward. + * + * It is legit to only own "create_other_timesheet" without "view_other_timesheet". + */ #[Route(path: '/team/timesheet')] -#[IsGranted('view_other_timesheet')] final class TimesheetTeamController extends TimesheetAbstractController { #[Route(path: '/', defaults: ['page' => 1], name: 'admin_timesheet', methods: ['GET'])] diff --git a/src/Form/DataTransformer/TagArrayToStringTransformer.php b/src/Form/DataTransformer/TagArrayToStringTransformer.php index eb00e494..1ec22a75 100644 --- a/src/Form/DataTransformer/TagArrayToStringTransformer.php +++ b/src/Form/DataTransformer/TagArrayToStringTransformer.php @@ -14,32 +14,30 @@ use App\Repository\TagRepository; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; +/** + * @implements DataTransformerInterface, string> + */ final class TagArrayToStringTransformer implements DataTransformerInterface { - private bool $create = true; - - public function __construct(private TagRepository $tagRepository) + public function __construct( + private readonly TagRepository $tagRepository, + private readonly bool $create + ) { } - public function setCreate(bool $create): void - { - $this->create = $create; - } - /** * Transforms an array of tags to a string. * - * @param Tag[]|null $tags - * @return string + * @param Tag[]|null $value */ - public function transform(mixed $tags): mixed + public function transform(mixed $value): string { - if (empty($tags)) { + if (empty($value)) { return ''; } - return implode(', ', $tags); + return implode(', ', $value); } /** @@ -47,18 +45,17 @@ final class TagArrayToStringTransformer implements DataTransformerInterface * * @see \Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform() * - * @param string|null $stringOfTags + * @param string|null $value * @return Tag[] * @throws TransformationFailedException */ - public function reverseTransform(mixed $stringOfTags): mixed + public function reverseTransform(mixed $value): mixed { // check for empty tag list - if ('' === $stringOfTags || null === $stringOfTags) { + if ('' === $value || null === $value) { return []; } - - $names = array_filter(array_unique(array_map('trim', explode(',', $stringOfTags)))); + $names = array_filter(array_unique(array_map('trim', explode(',', $value)))); // get the current tags and find the new ones that should be created $tags = $this->tagRepository->findBy(['name' => $names]); @@ -69,9 +66,9 @@ final class TagArrayToStringTransformer implements DataTransformerInterface foreach ($newNames as $name) { $tag = new Tag(); $tag->setName(mb_substr($name, 0, 100)); - $tags[] = $tag; + $this->tagRepository->saveTag($tag); - // new tags persist automatically thanks to the cascade={"persist"} + $tags[] = $tag; } } diff --git a/src/Form/FormTrait.php b/src/Form/FormTrait.php index e3a6a6c0..0ff9598b 100644 --- a/src/Form/FormTrait.php +++ b/src/Form/FormTrait.php @@ -73,14 +73,13 @@ trait FormTrait if ($isNew && \is_int($project)) { /** @var Project $project */ $project = $repo->find($project); - if ($project === null) { - throw new \Exception('Unknown project'); - } - if (!$project->getCustomer()->isVisible()) { - $customer = null; - $project = null; - } elseif (!$project->isVisible()) { - $project = null; + if ($project !== null) { + if (!$project->getCustomer()->isVisible()) { + $customer = null; + $project = null; + } elseif (!$project->isVisible()) { + $project = null; + } } } diff --git a/src/Form/Toolbar/CustomerToolbarForm.php b/src/Form/Toolbar/CustomerToolbarForm.php index 520b7a8d..534e7bd0 100644 --- a/src/Form/Toolbar/CustomerToolbarForm.php +++ b/src/Form/Toolbar/CustomerToolbarForm.php @@ -42,7 +42,7 @@ final class CustomerToolbarForm extends AbstractType $countries = $qb->getQuery()->getSingleColumnResult(); $choices = []; foreach ($countries as $country) { - if (\is_string($country) && \is_string($options['locale'])) { + if (\is_string($country) && $country !== '' && \is_string($options['locale'])) { $choices[$country] = Countries::getName($country, $options['locale']); } } diff --git a/src/Form/Type/DateRangeType.php b/src/Form/Type/DateRangeType.php index 86be426f..35443f71 100644 --- a/src/Form/Type/DateRangeType.php +++ b/src/Form/Type/DateRangeType.php @@ -65,7 +65,6 @@ final class DateRangeType extends AbstractType return ['pattern' => $pattern . self::DATE_SPACER . $pattern]; }); - } public function buildView(FormView $view, FormInterface $form, array $options): void diff --git a/src/Form/Type/TagsInputType.php b/src/Form/Type/TagsInputType.php index bd3837b2..2f7bfa40 100644 --- a/src/Form/Type/TagsInputType.php +++ b/src/Form/Type/TagsInputType.php @@ -10,6 +10,7 @@ namespace App\Form\Type; use App\Form\DataTransformer\TagArrayToStringTransformer; +use App\Repository\TagRepository; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -24,18 +25,17 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; */ final class TagsInputType extends AbstractType { - public function __construct(private TagArrayToStringTransformer $transformer, private UrlGeneratorInterface $router) + public function __construct( + private readonly TagRepository $tagRepository, + private readonly UrlGeneratorInterface $router + ) { } public function buildForm(FormBuilderInterface $builder, array $options): void { - if ($options['allow_create'] === false) { - $this->transformer->setCreate(false); - } - $builder->addModelTransformer(new CollectionToArrayTransformer(), true); - $builder->addModelTransformer($this->transformer, true); + $builder->addModelTransformer(new TagArrayToStringTransformer($this->tagRepository, (bool) $options['allow_create']), true); } public function configureOptions(OptionsResolver $resolver): void @@ -48,6 +48,7 @@ final class TagsInputType extends AbstractType 'allow_create' => false, 'label' => 'tag', ]); + $resolver->setAllowedTypes('allow_create', 'bool'); } public function buildView(FormView $view, FormInterface $form, array $options): void diff --git a/src/Form/Type/TagsSelectType.php b/src/Form/Type/TagsSelectType.php index bc8c6ac4..455a6dcf 100644 --- a/src/Form/Type/TagsSelectType.php +++ b/src/Form/Type/TagsSelectType.php @@ -28,78 +28,47 @@ use Symfony\Component\OptionsResolver\OptionsResolver; */ final class TagsSelectType extends AbstractType { - public function __construct(private TagRepository $tagRepository) + public function __construct( + private readonly TagRepository $tagRepository + ) { } public function buildForm(FormBuilderInterface $builder, array $options): void { - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) { - if (!$options['allow_create']) { - return; - } + if (!$options['allow_create']) { + return; + } + + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + /** @var array $tagIds */ $tagIds = $event->getData(); if (!\is_array($tagIds)) { return; } - $ids = array_filter($tagIds, function ($tagId) { + + $tags = []; + foreach ($tagIds as $tagId) { + $tag = null; + if (is_numeric($tagId)) { - return true; + $tag = $this->tagRepository->find($tagId); } - return false; - }); - - // get the current tags and find the new ones that should be created - $tags = $this->tagRepository->findBy(['id' => $ids]); - - $foundIds = []; - foreach ($tags as $tag) { - $foundIds[] = (string) $tag->getId(); - } - - $newData = []; - /** @var array $newNames */ - $newNames = []; - foreach ($tagIds as $tag) { - if (!\in_array($tag, $foundIds, true)) { - $newNames[] = $tag; - } else { - $newData[] = $tag; - } - } - - // 1. in case someone is using tags like "1234" this can interfere with the ID - // 2. if we would load only visible tags, we would try to create new ones below - // and that would trigger the unique constraint - $tags = $this->tagRepository->findTagsByName($newNames, null); - $foundTagNames = []; - foreach ($tags as $tag) { - $newData[] = (string) $tag->getId(); - $foundTagNames[] = $tag->getName(); - } - - /** @var array $newNamesCreate */ - $newNamesCreate = array_udiff($newNames, $foundTagNames, function (mixed $userTag, mixed $existingTag) { - if (!\is_string($userTag) || !\is_string($existingTag)) { - return -1; + if ($tag === null) { + $tag = $this->tagRepository->findTagByName($tagId); } - if (mb_strtolower($userTag) === mb_strtolower($existingTag)) { - return 0; + if ($tag === null) { + $tag = new Tag(); + $tag->setName(mb_substr($tagId, 0, 100)); + $this->tagRepository->saveTag($tag); } - return strcmp($userTag, $existingTag); - }); - - foreach ($newNamesCreate as $name) { - $tag = new Tag(); - $tag->setName(mb_substr($name, 0, 100)); - $this->tagRepository->saveTag($tag); - $newData[] = $tag->getId(); + $tags[] = $tag->getId(); } - $event->setData($newData); + $event->setData($tags); }, 1000); } @@ -132,6 +101,8 @@ final class TagsSelectType extends AbstractType return $repo->getQueryBuilderForFormType($query); }; }); + + $resolver->setAllowedTypes('allow_create', 'bool'); } public function buildView(FormView $view, FormInterface $form, array $options): void diff --git a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php index 5549699d..a4111006 100644 --- a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php @@ -14,6 +14,8 @@ use App\Invoice\InvoiceModelHydrator; final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator { + private const DATE_PROCESS_FORMAT = 'Y-m-d h:i:s'; + public function hydrate(InvoiceModel $model): array { $currency = $model->getCurrency(); @@ -24,7 +26,9 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator $values = [ 'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()), + 'invoice.due_date_process' => $model->getDueDate()->format(self::DATE_PROCESS_FORMAT), // since 2.14 'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()), + 'invoice.date_process' => $model->getInvoiceDate()->format(self::DATE_PROCESS_FORMAT), // since 2.14 'invoice.number' => $model->getInvoiceNumber(), 'invoice.currency' => $currency, 'invoice.language' => $model->getTemplate()->getLanguage(), // since 1.9 @@ -55,11 +59,13 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator 'query.begin' => '', 'query.begin_day' => '', + 'query.begin_process' => null, // since 2.14 'query.begin_month' => '', 'query.begin_month_number' => '', 'query.begin_year' => '', 'query.end' => '', // since 1.9 'query.end_day' => '', // since 1.9 + 'query.end_process' => null, // since 2.14 'query.end_month' => '', // since 1.9 'query.end_month_number' => '', // since 1.9 'query.end_year' => '', // since 1.9 @@ -82,6 +88,7 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator 'query.year' => $begin->format('Y'), // @deprecated - but impossible to delete 'query.begin' => $formatter->getFormattedDateTime($begin), + 'query.begin_process' => $begin->format(self::DATE_PROCESS_FORMAT), // since 2.14 'query.begin_day' => $begin->format('d'), 'query.begin_month' => $formatter->getFormattedMonthName($begin), 'query.begin_month_number' => $begin->format('m'), @@ -93,13 +100,10 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator if ($end !== null) { $values = array_merge($values, [ 'query.end' => $formatter->getFormattedDateTime($end), - // since 1.9 + 'query.end_process' => $end->format(self::DATE_PROCESS_FORMAT), // since 2.14 'query.end_day' => $end->format('d'), - // since 1.9 'query.end_month' => $formatter->getFormattedMonthName($end), - // since 1.9 'query.end_month_number' => $end->format('m'), - // since 1.9 'query.end_year' => $end->format('Y'), ]); } @@ -145,7 +149,9 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator if ($min !== null && $max !== null) { $values = array_merge($values, [ 'invoice.first' => $formatter->getFormattedDateTime($min->getBegin()), + 'invoice.first_process' => $min->getBegin()?->format(self::DATE_PROCESS_FORMAT), // since 2.14 'invoice.last' => $formatter->getFormattedDateTime($max->getEnd()), + 'invoice.last_process' => $max->getEnd()?->format(self::DATE_PROCESS_FORMAT), // since 2.14 ]); } diff --git a/src/Saml/SamlProvider.php b/src/Saml/SamlProvider.php index 7909fe98..f152d509 100644 --- a/src/Saml/SamlProvider.php +++ b/src/Saml/SamlProvider.php @@ -12,6 +12,7 @@ namespace App\Saml; use App\Configuration\SamlConfigurationInterface; use App\Entity\User; use App\Repository\UserRepository; +use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -24,7 +25,8 @@ final class SamlProvider public function __construct( private readonly UserRepository $repository, private readonly UserProviderInterface $userProvider, - private readonly SamlConfigurationInterface $configuration + private readonly SamlConfigurationInterface $configuration, + private readonly LoggerInterface $logger ) { } @@ -37,7 +39,8 @@ final class SamlProvider /** @var User $user */ $user = $this->userProvider->loadUserByIdentifier($token->getUserIdentifier()); } - } catch (UserNotFoundException $e) { + } catch (UserNotFoundException $ex) { + $this->logger->error($ex->getMessage()); } try { @@ -49,6 +52,7 @@ final class SamlProvider $this->repository->saveUser($user); } catch (\Exception $ex) { + $this->logger->error($ex->getMessage()); throw new AuthenticationException( sprintf('Failed creating or hydrating user "%s": %s', $token->getUserIdentifier(), $ex->getMessage()) ); @@ -110,7 +114,8 @@ final class SamlProvider if (method_exists($user, $setter)) { $user->$setter($value); } else { - throw new \RuntimeException('Invalid mapping field given: ' . $field); + // this should never happen, because it is validated when the container is built + throw new \RuntimeException('Invalid SAML mapping field: ' . $field); } } @@ -136,7 +141,7 @@ final class SamlProvider if ($part[0] === '$') { $key = substr($part, 1); if (!\array_key_exists($key, $attributes)) { - throw new \RuntimeException('Missing user attribute: ' . $key); + throw new \RuntimeException('Missing SAML attribute in response: ' . $key); } if (\is_array($attributes[$key]) && isset($attributes[$key][0])) { diff --git a/src/Voter/ProjectVoter.php b/src/Voter/ProjectVoter.php index 5aa53c84..d48d6892 100644 --- a/src/Voter/ProjectVoter.php +++ b/src/Voter/ProjectVoter.php @@ -69,7 +69,7 @@ final class ProjectVoter extends Voter } // those cannot be assigned to teams - if (\in_array($attribute, ['create', 'delete'])) { + if (\in_array($attribute, ['create', 'delete'], true)) { return false; } diff --git a/templates/macros/widgets.html.twig b/templates/macros/widgets.html.twig index d8d26e3b..fa6356d0 100644 --- a/templates/macros/widgets.html.twig +++ b/templates/macros/widgets.html.twig @@ -290,7 +290,7 @@ {% macro meta_field_value(entity, field) %} {% set metaField = entity.metaField(field.name) %} - {% if not metaField is null and metaField.value is not null and metaField.value is not empty %} + {% if not metaField is null %} {% set metaField = metaField.merge(field) %} {{ _self.form_type_value(metaField.type, metaField.value, entity) }} {% endif %} diff --git a/templates/user/contract.html.twig b/templates/user/contract.html.twig index 7fa30c21..1956b8a0 100644 --- a/templates/user/contract.html.twig +++ b/templates/user/contract.html.twig @@ -8,7 +8,6 @@
{{ 'work_times_should'|trans }} -
{{ 'work_times_should.help'|trans }}
{{ form_row(form.workHoursMonday) }} {{ form_row(form.workHoursTuesday) }} {{ form_row(form.workHoursWednesday) }} diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php index fda791bd..dd2e1077 100644 --- a/tests/API/TimesheetControllerTest.php +++ b/tests/API/TimesheetControllerTest.php @@ -465,7 +465,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'hourlyRate' => 127, 'billable' => false ]; - $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets', 'POST', [], $json); $this->assertTrue($client->getResponse()->isSuccessful()); $content = $client->getResponse()->getContent(); @@ -494,7 +496,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'hourlyRate' => 127, 'billable' => true ]; - $this->request($client, '/api/timesheets?full=true', 'POST', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets?full=true', 'POST', [], $json); $this->assertTrue($client->getResponse()->isSuccessful()); $content = $client->getResponse()->getContent(); @@ -526,7 +530,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'), 'description' => 'foo', ]; - $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets', 'POST', [], $json); $this->assertTrue($client->getResponse()->isSuccessful()); $content = $client->getResponse()->getContent(); @@ -571,7 +577,60 @@ class TimesheetControllerTest extends APIControllerBaseTest 'end' => (new \DateTime())->format('Y-m-d H:m:s'), 'description' => 'foo', ]; - $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets', 'POST', [], $json); + $this->assertApiCallValidationError($client->getResponse(), ['project']); + } + + public function testPostActionWithUnknownActivity(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + + $em = $this->getEntityManager(); + $customer = new Customer('foo-bar-1'); + $customer->setVisible(false); + $customer->setCountry('DE'); + $customer->setTimezone('Europe/Berlin'); + $em->persist($customer); + $project = new Project(); + $project->setName('foo-bar-2'); + $project->setVisible(true); + $project->setCustomer($customer); + $em->persist($project); + + $data = [ + 'begin' => (new \DateTime('- 8 hours'))->format('Y-m-d H:m:s'), + 'end' => (new \DateTime())->format('Y-m-d H:m:s'), + 'project' => $project->getId(), + 'activity' => 99, + ]; + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets', 'POST', [], $json); + $this->assertApiCallValidationError($client->getResponse(), ['project']); + } + + public function testPostActionWithNonExistingProject(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + + $em = $this->getEntityManager(); + $activity = new Activity(); + $activity->setName('foo-bar-3'); + $activity->setVisible(true); + $em->persist($activity); + $em->flush(); + + $data = [ + 'begin' => (new \DateTime('- 8 hours'))->format('Y-m-d H:m:s'), + 'end' => (new \DateTime())->format('Y-m-d H:m:s'), + 'project' => 99, + 'activity' => $activity->getId(), + ]; + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets', 'POST', [], $json); $this->assertApiCallValidationError($client->getResponse(), ['project']); } @@ -605,7 +664,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'end' => (new \DateTime())->format('Y-m-d H:m'), 'description' => 'foo', ]; - $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets', 'POST', [], $json); $this->assertApiCallValidationError($client->getResponse(), ['activity']); } @@ -635,7 +696,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'end' => (new \DateTime())->format('Y-m-d H:m'), 'description' => 'foo', ]; - $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets', 'POST', [], $json); $this->assertTrue($client->getResponse()->isSuccessful()); $content = $client->getResponse()->getContent(); @@ -674,7 +737,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'description' => 'foo', 'billable' => true, ]; - $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets', 'POST', [], $json); $this->assertTrue($client->getResponse()->isSuccessful()); $content = $client->getResponse()->getContent(); @@ -700,7 +765,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'description' => 'foo', 'billable' => false, ]; - $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], $json); $this->assertTrue($client->getResponse()->isSuccessful()); $content = $client->getResponse()->getContent(); @@ -739,7 +806,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'description' => 'foo', 'exported' => true, ]; - $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], $json); $response = $client->getResponse(); $this->assertApiResponseAccessDenied($response); } @@ -761,7 +830,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'end' => (new \DateTime('- 1 hours'))->format('Y-m-d H:m'), 'description' => 'foo', ]; - $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], $json); $response = $client->getResponse(); self::assertEquals(400, $response->getStatusCode()); @@ -1066,7 +1137,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'description' => 'foo', 'tags' => ['another', 'testing', 'bar'] ]; - $this->request($client, '/api/timesheets/' . $id, 'PATCH', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets/' . $id, 'PATCH', [], $json); $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH'); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -1100,7 +1173,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'description' => 'foo', 'tags' => ['another', 'testing', 'bar'] ]; - $this->request($client, '/api/timesheets/' . $id, 'PATCH', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets/' . $id, 'PATCH', [], $json); $begin = new \DateTime('2019-11-27 13:55:00'); $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH', ['begin' => $begin->format(BaseApiController::DATE_FORMAT_PHP)]); @@ -1211,7 +1286,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'fixedRate' => 2016, 'hourlyRate' => 127 ]; - $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets', 'POST', [], $json); $this->assertTrue($client->getResponse()->isSuccessful()); $content = $client->getResponse()->getContent(); @@ -1343,7 +1420,9 @@ class TimesheetControllerTest extends APIControllerBaseTest 'name' => 'metatestmock', 'value' => 'another,testing,bar' ]; - $this->request($client, '/api/timesheets/' . $id . '/meta', 'PATCH', [], json_encode($data)); + $json = json_encode($data); + self::assertIsString($json); + $this->request($client, '/api/timesheets/' . $id . '/meta', 'PATCH', [], $json); $this->assertTrue($client->getResponse()->isSuccessful()); diff --git a/tests/Form/DataTransformer/TagArrayToStringTransformerTest.php b/tests/Form/DataTransformer/TagArrayToStringTransformerTest.php index 324ad4b2..b78b5440 100644 --- a/tests/Form/DataTransformer/TagArrayToStringTransformerTest.php +++ b/tests/Form/DataTransformer/TagArrayToStringTransformerTest.php @@ -28,7 +28,7 @@ class TagArrayToStringTransformerTest extends TestCase $repository = $this->getMockBuilder(TagRepository::class)->disableOriginalConstructor()->getMock(); - $sut = new TagArrayToStringTransformer($repository); + $sut = new TagArrayToStringTransformer($repository, true); $this->assertEquals('', $sut->transform([])); $this->assertEquals('', $sut->transform(null)); @@ -45,10 +45,12 @@ class TagArrayToStringTransformerTest extends TestCase (new Tag())->setName('bar'), ]; - $repository = $this->getMockBuilder(TagRepository::class)->onlyMethods(['findBy'])->disableOriginalConstructor()->getMock(); + $repository = $this->getMockBuilder(TagRepository::class) + ->onlyMethods(['findBy', 'saveTag']) + ->disableOriginalConstructor()->getMock(); $repository->expects($this->once())->method('findBy')->willReturn($results); - $sut = new TagArrayToStringTransformer($repository); + $sut = new TagArrayToStringTransformer($repository, true); $this->assertEquals([], $sut->reverseTransform('')); $this->assertEquals([], $sut->reverseTransform(null)); diff --git a/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php index 72484f66..4f7a6ed1 100644 --- a/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php @@ -34,7 +34,9 @@ class InvoiceModelDefaultHydratorTest extends TestCase { $keys = [ 'invoice.due_date', + 'invoice.due_date_process', 'invoice.date', + 'invoice.date_process', 'invoice.number', 'invoice.currency', 'invoice.currency_symbol', @@ -47,7 +49,9 @@ class InvoiceModelDefaultHydratorTest extends TestCase 'invoice.total_time', 'invoice.duration_decimal', 'invoice.first', + 'invoice.first_process', 'invoice.last', + 'invoice.last_process', 'invoice.total', 'invoice.total_nc', 'invoice.total_plain', @@ -68,11 +72,13 @@ class InvoiceModelDefaultHydratorTest extends TestCase 'query.month_number', 'query.year', 'query.begin', + 'query.begin_process', 'query.begin_day', 'query.begin_month', 'query.begin_month_number', 'query.begin_year', 'query.end', + 'query.end_process', 'query.end_day', 'query.end_month', 'query.end_month_number', diff --git a/tests/Invoice/Renderer/DebugRendererTest.php b/tests/Invoice/Renderer/DebugRendererTest.php index 30a97de9..a35755c5 100644 --- a/tests/Invoice/Renderer/DebugRendererTest.php +++ b/tests/Invoice/Renderer/DebugRendererTest.php @@ -79,7 +79,9 @@ class DebugRendererTest extends TestCase { $keys = [ 'invoice.due_date', + 'invoice.due_date_process', 'invoice.date', + 'invoice.date_process', 'invoice.number', 'invoice.currency', 'invoice.currency_symbol', @@ -92,7 +94,9 @@ class DebugRendererTest extends TestCase 'invoice.total_time', 'invoice.duration_decimal', 'invoice.first', + 'invoice.first_process', 'invoice.last', + 'invoice.last_process', 'invoice.total', 'invoice.total_nc', 'invoice.total_plain', @@ -113,11 +117,13 @@ class DebugRendererTest extends TestCase 'query.month_number', 'query.year', 'query.begin', + 'query.begin_process', 'query.begin_day', 'query.begin_month', 'query.begin_month_number', 'query.begin_year', 'query.end', + 'query.end_process', 'query.end_day', 'query.end_month', 'query.end_month_number', diff --git a/tests/Repository/Query/ExportQueryTest.php b/tests/Repository/Query/ExportQueryTest.php index 64192fbc..f11256a0 100644 --- a/tests/Repository/Query/ExportQueryTest.php +++ b/tests/Repository/Query/ExportQueryTest.php @@ -38,10 +38,10 @@ class ExportQueryTest extends TimesheetQueryTest public function assertMarkAsExported(ExportQuery $sut): void { - $this->assertTrue($sut->isMarkAsExported()); - - $sut->setMarkAsExported(false); $this->assertFalse($sut->isMarkAsExported()); + + $sut->setMarkAsExported(true); + $this->assertTrue($sut->isMarkAsExported()); } public function assertRenderer(ExportQuery $sut): void diff --git a/tests/Saml/SamlProviderTest.php b/tests/Saml/SamlProviderTest.php index 1a7f2ce0..7539e2be 100644 --- a/tests/Saml/SamlProviderTest.php +++ b/tests/Saml/SamlProviderTest.php @@ -17,6 +17,7 @@ use App\Saml\SamlProvider; use App\Tests\Configuration\TestConfigLoader; use App\Tests\Mocks\SystemConfigurationFactory; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -57,7 +58,7 @@ class SamlProviderTest extends TestCase $userProvider->method('loadUserByIdentifier')->willReturn(new User()); } - $provider = new SamlProvider($repository, $userProvider, $samlConfig); + $provider = new SamlProvider($repository, $userProvider, $samlConfig, $this->createMock(LoggerInterface::class)); return $provider; } @@ -107,7 +108,7 @@ class SamlProviderTest extends TestCase public function testAuthenticateThrowsAuthenticationException(): void { $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Failed creating or hydrating user "foo1@example.com": Missing user attribute: Email'); + $this->expectExceptionMessage('Failed creating or hydrating user "foo1@example.com": Missing SAML attribute in response: Email'); $user = new User(); $user->setAuth(User::AUTH_SAML); diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 7b904154..7213babc 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -502,11 +502,6 @@ parameters: count: 6 path: API/TimesheetControllerTest.php - - - message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#" - count: 14 - path: API/TimesheetControllerTest.php - - message: "#^Cannot access offset 'enabled' on mixed\\.$#" count: 1 diff --git a/translations/messages.ar.xlf b/translations/messages.ar.xlf index c36eca0d..98daeba9 100644 --- a/translations/messages.ar.xlf +++ b/translations/messages.ar.xlf @@ -1466,7 +1466,7 @@ مرَض - work_times_should + Expected number of hours الساعات المستهدَفة diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 60253ee1..fc9d4f19 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -1531,10 +1531,6 @@ work_times_result Odpracované hodiny - - work_times_should.help - Zde nakonfigurované hodnoty se používají k výpočtu hodinového účtu. - work_times_should.none_configured Pro tohoto uživatele nebyly v nastavení pracovní smlouvy nakonfigurovány žádné cílové hodiny. @@ -1548,7 +1544,7 @@ Je-hodin - work_times_should + Expected number of hours Cílové hodiny diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 9790b431..99aa68cd 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -647,12 +647,8 @@ Arbeitszeitkonto - work_times_should - Soll-Stunden - - - work_times_should.help - Die hier konfigurierten Werte werden zur Berechnung des Stundenkontos verwendet. + Expected number of hours + Erwartete Stundenzahl work_times_should.none_configured diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 540efeda..eba1a7d3 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -647,12 +647,8 @@ Account working times - work_times_should - Target-hours - - - work_times_should.help - The values configured here are used to calculate the hourly account. + Expected number of hours + Expected number of hours work_times_should.none_configured diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 8ab003d5..5d0d70ca 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -1496,13 +1496,9 @@ Evaluación - work_times_should + Expected number of hours Horas de objetivo - - work_times_should.help - Los valores configurados aquí se utilizan para calcular las horas. - completed_month_pdf_note Este documento sirve como prueba de las horas trabajadas. diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 344feeb4..6eafeb0f 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -1504,7 +1504,7 @@ Évaluation - work_times_should + Expected number of hours Heures cibles @@ -1535,10 +1535,6 @@ Working times Heures de travail - - work_times_should.help - Les valeurs configurées ici sont utilisées pour calculer le compte horaire. - absence_other.intro Evènements familiaux, déménagement, catastrophes climatiques, etc. diff --git a/translations/messages.he.xlf b/translations/messages.he.xlf index 07da78ec..6967ef5c 100644 --- a/translations/messages.he.xlf +++ b/translations/messages.he.xlf @@ -1446,10 +1446,6 @@ Account working times זמני העבודה בחשבון - - work_times_should.help - הערכים שהוגדרו כאן משמשים לחישוב החשבון השעתי. - work_times_is שעות בפועל @@ -1487,7 +1483,7 @@ סקירה - work_times_should + Expected number of hours שעות יעד diff --git a/translations/messages.hr.xlf b/translations/messages.hr.xlf index bd79128f..1af43a0f 100644 --- a/translations/messages.hr.xlf +++ b/translations/messages.hr.xlf @@ -1500,13 +1500,9 @@ Ovaj dokument služi kao dokaz o odrađenim satima. - work_times_should + Expected number of hours Ciljani sati - - work_times_should.help - Ovdje konfigurirane vrijednosti se koriste za računanje računa po satu. - manual_bookings Ručne rezervacije diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index f0b55e9b..b00abf65 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -1491,7 +1491,7 @@ Nessun obiettivo di ore è stato configurato per questo utente nelle impostazioni del contratto di lavoro. - work_times_should + Expected number of hours Obiettivo di ore @@ -1522,10 +1522,6 @@ completed_month_pdf_note Questo documento serve come prova delle ore lavorate. - - work_times_should.help - I valori configurati qui vengono usati per calcolare il conto orario. - confirmed_by_at Confermato da %user% il %date% diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 2204cc98..bf2b54d0 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1484,7 +1484,7 @@ Arbeidsovereenkomst - work_times_should + Expected number of hours Streefuren @@ -1507,10 +1507,6 @@ manual_bookings.work_contract_intro Handmatige boeken kunnen niet achtereenvolgens worden gewijzigd of verwijderd! - - work_times_should.help - De hier geconfigureerde waarden worden gebruikt om de rekening per uur te berekenen. - work_times_should.none_configured Voor deze gebruiker zijn geen streefuren geconfigureerd in de instellingen van de arbeidsovereenkomst. diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 3bfd9621..c2ee63a4 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -1512,7 +1512,7 @@ Czas pracy - work_times_should + Expected number of hours Godziny docelowe diff --git a/translations/messages.pt.xlf b/translations/messages.pt.xlf index 9dd856f3..d5d9126b 100644 --- a/translations/messages.pt.xlf +++ b/translations/messages.pt.xlf @@ -1688,13 +1688,9 @@ Avaliação - work_times_should + Expected number of hours Horas-alvo - - work_times_should.help - Os valores configurados aqui são usados para calcular a hora da conta. - work_times_should.none_configured Nenhuma hora-alvo foi configurada para esse utilizador nas configurações do contrato de trabalho. diff --git a/translations/messages.pt_BR.xlf b/translations/messages.pt_BR.xlf index 98fadd97..473d7bce 100644 --- a/translations/messages.pt_BR.xlf +++ b/translations/messages.pt_BR.xlf @@ -1496,13 +1496,9 @@ Horários de trabalho da conta - work_times_should + Expected number of hours Horas-alvo - - work_times_should.help - Os valores configurados aqui são usados para calcular a hora da conta. - work_times_is Is-horas diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 8d373b99..82c1d4f7 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -1329,10 +1329,6 @@ work_times Время работы - - work_times_should.help - Настроенные здесь значения используются для расчета почасового счета. - work_times_is Ис-часы @@ -1406,7 +1402,7 @@ Создано - work_times_should + Expected number of hours Целевые часы diff --git a/translations/messages.sk.xlf b/translations/messages.sk.xlf index 3da8c9b2..325ea153 100644 --- a/translations/messages.sk.xlf +++ b/translations/messages.sk.xlf @@ -1355,13 +1355,9 @@ Táto faktúra je generovaná s touto šablónou. - work_times_should + Expected number of hours Cieľové hodiny - - work_times_should.help - Nakonfigurované hodnoty se používajú k výpočtu hodinového účtu. - holiday Dovolenka diff --git a/translations/messages.tr.xlf b/translations/messages.tr.xlf index 8730b449..dfe47cfd 100644 --- a/translations/messages.tr.xlf +++ b/translations/messages.tr.xlf @@ -1516,7 +1516,7 @@ İnceleme - work_times_should + Expected number of hours Hedef saat @@ -1531,10 +1531,6 @@ manual_bookings Elle rezervasyonlar - - work_times_should.help - Burada yapılandırılan değerler saatlik hesabı hesaplamak için kullanılır. - work_times_should.none_configured İş sözleşmesi ayarlarında bu kullanıcı için herhangi bir hedef saat yapılandırılmamış. diff --git a/translations/messages.uk.xlf b/translations/messages.uk.xlf index b0b98c13..5f7f09aa 100644 --- a/translations/messages.uk.xlf +++ b/translations/messages.uk.xlf @@ -1426,10 +1426,6 @@ work_times Робочі години - - work_times_should.help - Налаштовані тут значення використовуються для погодинного розрахунку облікового запису. - deleted Видалено @@ -1443,7 +1439,7 @@ Після завершення місяця користувач більше не зможе створювати або редагувати час для цього періоду. - work_times_should + Expected number of hours Цільові години diff --git a/translations/messages.zh_CN.xlf b/translations/messages.zh_CN.xlf index fca6a6f7..59694453 100644 --- a/translations/messages.zh_CN.xlf +++ b/translations/messages.zh_CN.xlf @@ -1495,10 +1495,6 @@ work_times_should.none_configured 在雇佣合同设置中未为此用户配置目标工时。 - - work_times_should.help - 这里配置的数值用于计算每小时的账户。 - complete_month.help 这将锁定所选日期之前一年中的所有日期。 用户将无法再创建或编辑锁定期间的时间。 @@ -1528,7 +1524,7 @@ 工作时间 - work_times_should + Expected number of hours 目标小时数 diff --git a/translations/messages.zh_Hant.xlf b/translations/messages.zh_Hant.xlf index 0e1cab8a..4bd27430 100644 --- a/translations/messages.zh_Hant.xlf +++ b/translations/messages.zh_Hant.xlf @@ -1047,7 +1047,7 @@ 病假 - work_times_should + Expected number of hours 目標時數 @@ -1078,10 +1078,6 @@ api_password.missing_title 無法使用 API - - work_times_should.help - 這裡所設定的數值會用於計算鐘點。 - profile.2fa_confirmation 請確認代碼,以啟用該帳號的雙重要素驗證。