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)
This commit is contained in:
Kevin Papst
2024-04-05 12:38:21 +02:00
committed by GitHub
parent 19b2d47591
commit b6c98f871d
45 changed files with 318 additions and 317 deletions

159
composer.lock generated
View File

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

View File

@@ -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\\<mixed,mixed\\>\\:\\:reverseTransform\\(\\)$#"
count: 1
path: src/Form/DataTransformer/TagArrayToStringTransformer.php
-
message: "#^Parameter \\#1 \\$tags \\(array\\<App\\\\Entity\\\\Tag\\>\\|null\\) of method App\\\\Form\\\\DataTransformer\\\\TagArrayToStringTransformer\\:\\:transform\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Form\\\\DataTransformerInterface\\<mixed,mixed\\>\\:\\: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

View File

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

View File

@@ -255,6 +255,8 @@ abstract class AbstractController extends BaseAbstractController implements Serv
}
/**
* Use "performSearch=1" to skip loading session searches.
*
* @param array<string> $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);

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,32 +14,30 @@ use App\Repository\TagRepository;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @implements DataTransformerInterface<array<Tag>, 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@
<fieldset class="form-fieldset form-fieldset-light">
<legend>{{ 'work_times_should'|trans }}</legend>
<div class="form-text mb-3 help-text">{{ 'work_times_should.help'|trans }}</div>
{{ form_row(form.workHoursMonday) }}
{{ form_row(form.workHoursTuesday) }}
{{ form_row(form.workHoursWednesday) }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1466,7 +1466,7 @@
<target state="translated">مرَض</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">الساعات المستهدَفة</target>
</trans-unit>
<trans-unit id="AXr_aHI" resname="completed_month_pdf_note">

View File

@@ -1531,10 +1531,6 @@
<source>work_times_result</source>
<target state="translated">Odpracované hodiny</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Zde nakonfigurované hodnoty se používají k výpočtu hodinového účtu.</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source>
<target state="translated">Pro tohoto uživatele nebyly v nastavení pracovní smlouvy nakonfigurovány žádné cílové hodiny.</target>
@@ -1548,7 +1544,7 @@
<target state="translated">Je-hodin</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Cílové hodiny</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at">

View File

@@ -647,12 +647,8 @@
<target>Arbeitszeitkonto</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<target>Soll-Stunden</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target>Die hier konfigurierten Werte werden zur Berechnung des Stundenkontos verwendet.</target>
<source>Expected number of hours</source>
<target>Erwartete Stundenzahl</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source>

View File

@@ -647,12 +647,8 @@
<target state="translated">Account working times</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<target>Target-hours</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target>The values configured here are used to calculate the hourly account.</target>
<source>Expected number of hours</source>
<target>Expected number of hours</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source>

View File

@@ -1496,13 +1496,9 @@
<target state="translated">Evaluación</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Horas de objetivo</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Los valores configurados aquí se utilizan para calcular las horas.</target>
</trans-unit>
<trans-unit id="AXr_aHI" resname="completed_month_pdf_note">
<source>completed_month_pdf_note</source>
<target state="translated">Este documento sirve como prueba de las horas trabajadas.</target>

View File

@@ -1504,7 +1504,7 @@
<target state="translated">Évaluation</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Heures cibles</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
@@ -1535,10 +1535,6 @@
<source>Working times</source>
<target state="translated">Heures de travail</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Les valeurs configurées ici sont utilisées pour calculer le compte horaire.</target>
</trans-unit>
<trans-unit id="YPO294a" resname="absence_other.intro" xml:space="preserve">
<source>absence_other.intro</source>
<target state="translated">Evènements familiaux, déménagement, catastrophes climatiques, etc.</target>

View File

@@ -1446,10 +1446,6 @@
<source>Account working times</source>
<target state="translated">זמני העבודה בחשבון</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">הערכים שהוגדרו כאן משמשים לחישוב החשבון השעתי.</target>
</trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is">
<source>work_times_is</source>
<target state="translated">שעות בפועל</target>
@@ -1487,7 +1483,7 @@
<target state="translated">סקירה</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">שעות יעד</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">

View File

@@ -1500,13 +1500,9 @@
<target state="translated">Ovaj dokument služi kao dokaz o odrađenim satima.</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Ciljani sati</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Ovdje konfigurirane vrijednosti se koriste za računanje računa po satu.</target>
</trans-unit>
<trans-unit id="wZRRqg0" resname="manual_bookings">
<source>manual_bookings</source>
<target state="translated">Ručne rezervacije</target>

View File

@@ -1491,7 +1491,7 @@
<target state="final">Nessun obiettivo di ore è stato configurato per questo utente nelle impostazioni del contratto di lavoro.</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should" approved="yes">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="final">Obiettivo di ore</target>
</trans-unit>
<trans-unit id="znIJiOw" resname="manual_bookings.duration_help" approved="yes">
@@ -1522,10 +1522,6 @@
<source>completed_month_pdf_note</source>
<target state="final">Questo documento serve come prova delle ore lavorate.</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help" approved="yes">
<source>work_times_should.help</source>
<target state="final">I valori configurati qui vengono usati per calcolare il conto orario.</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at" approved="yes">
<source>confirmed_by_at</source>
<target state="final">Confermato da %user% il %date%</target>

View File

@@ -1484,7 +1484,7 @@
<target state="translated">Arbeidsovereenkomst</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Streefuren</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
@@ -1507,10 +1507,6 @@
<source>manual_bookings.work_contract_intro</source>
<target state="translated">Handmatige boeken kunnen niet achtereenvolgens worden gewijzigd of verwijderd!</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">De hier geconfigureerde waarden worden gebruikt om de rekening per uur te berekenen.</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source>
<target state="translated">Voor deze gebruiker zijn geen streefuren geconfigureerd in de instellingen van de arbeidsovereenkomst.</target>

View File

@@ -1512,7 +1512,7 @@
<target state="translated">Czas pracy</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Godziny docelowe</target>
</trans-unit>
<trans-unit id="t1r372x" resname="invisible">

View File

@@ -1688,13 +1688,9 @@
<target state="translated">Avaliação</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Horas-alvo</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Os valores configurados aqui são usados para calcular a hora da conta.</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source>
<target state="translated">Nenhuma hora-alvo foi configurada para esse utilizador nas configurações do contrato de trabalho.</target>

View File

@@ -1496,13 +1496,9 @@
<target state="translated">Horários de trabalho da conta</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Horas-alvo</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Os valores configurados aqui são usados para calcular a hora da conta.</target>
</trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is">
<source>work_times_is</source>
<target state="translated">Is-horas</target>

View File

@@ -1329,10 +1329,6 @@
<source>work_times</source>
<target state="translated">Время работы</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Настроенные здесь значения используются для расчета почасового счета.</target>
</trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is">
<source>work_times_is</source>
<target state="needs-translation">Ис-часы</target>
@@ -1406,7 +1402,7 @@
<target state="translated">Создано</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Целевые часы</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">

View File

@@ -1355,13 +1355,9 @@
<target state="translated">Táto faktúra je generovaná s touto šablónou.</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Cieľové hodiny</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Nakonfigurované hodnoty se používajú k výpočtu hodinového účtu.</target>
</trans-unit>
<trans-unit id="gcFtM3o" resname="holiday">
<source>holiday</source>
<target state="translated">Dovolenka</target>

View File

@@ -1516,7 +1516,7 @@
<target state="translated">İnceleme</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Hedef saat</target>
</trans-unit>
<trans-unit id="mS4SSmt" resname="work_times">
@@ -1531,10 +1531,6 @@
<source>manual_bookings</source>
<target state="translated">Elle rezervasyonlar</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Burada yapılandırılan değerler saatlik hesabı hesaplamak için kullanılır.</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source>
<target state="translated">İş sözleşmesi ayarlarında bu kullanıcı için herhangi bir hedef saat yapılandırılmamış.</target>

View File

@@ -1426,10 +1426,6 @@
<source>work_times</source>
<target state="translated">Робочі години</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Налаштовані тут значення використовуються для погодинного розрахунку облікового запису.</target>
</trans-unit>
<trans-unit id="EYXzfTO" resname="deleted">
<source>deleted</source>
<target state="translated">Видалено</target>
@@ -1443,7 +1439,7 @@
<target state="needs-translation">Після завершення місяця користувач більше не зможе створювати або редагувати час для цього періоду.</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">Цільові години</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">

View File

@@ -1495,10 +1495,6 @@
<source>work_times_should.none_configured</source>
<target state="translated">在雇佣合同设置中未为此用户配置目标工时。</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">这里配置的数值用于计算每小时的账户。</target>
</trans-unit>
<trans-unit id="W8XQTkJ" resname="complete_month.help" xml:space="preserve">
<source>complete_month.help</source>
<target state="translated">这将锁定所选日期之前一年中的所有日期。 用户将无法再创建或编辑锁定期间的时间。</target>
@@ -1528,7 +1524,7 @@
<target state="translated">工作时间</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">目标小时数</target>
</trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is">

View File

@@ -1047,7 +1047,7 @@
<target state="translated">病假</target>
</trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source>
<source>Expected number of hours</source>
<target state="translated">目標時數</target>
</trans-unit>
<trans-unit id="c3d6p33" resname="invoice.total_working_time">
@@ -1078,10 +1078,6 @@
<source>api_password.missing_title</source>
<target state="translated">無法使用 API</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">這裡所設定的數值會用於計算鐘點。</target>
</trans-unit>
<trans-unit id="Y0PRG9Z" resname="profile.2fa_confirmation">
<source>profile.2fa_confirmation</source>
<target state="translated">請確認代碼,以啟用該帳號的雙重要素驗證。</target>