From 7219b3f421916bcf35b3247ff68b68c22cadd88e Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Fri, 12 Apr 2024 19:03:09 +0200 Subject: [PATCH] Release 2.15 (#4749) --- composer.json | 1 + composer.lock | 147 +++++++++---- config/packages/doctrine.yaml | 2 + config/packages/security.yaml | 3 +- phpstan.neon | 105 +-------- public/.htaccess | 49 +++-- src/API/ActionsController.php | 2 +- src/API/ActivityController.php | 2 +- src/API/ConfigurationController.php | 2 +- src/API/CustomerController.php | 2 +- src/API/ProjectController.php | 2 +- src/API/StatusController.php | 2 +- src/API/TagController.php | 2 +- src/API/TeamController.php | 2 +- src/API/TimesheetController.php | 10 +- src/API/UserController.php | 2 +- src/Configuration/SystemConfiguration.php | 15 +- src/Constants.php | 4 +- src/Controller/AbstractController.php | 2 +- src/Controller/DoctorController.php | 1 + src/Controller/PluginController.php | 2 +- .../TimesheetAbstractController.php | 6 +- src/Doctrine/Extensions/Date.php | 8 +- src/Doctrine/Extensions/Day.php | 8 +- src/Doctrine/Extensions/Month.php | 8 +- src/Doctrine/Extensions/Year.php | 8 +- src/Doctrine/UTCDateTimeType.php | 3 +- .../Hydrator/InvoiceItemDefaultHydrator.php | 27 ++- .../Hydrator/InvoiceModelActivityHydrator.php | 2 + .../Hydrator/InvoiceModelProjectHydrator.php | 2 + src/Repository/Query/ActivityQuery.php | 9 +- src/Repository/Query/ProjectQuery.php | 13 +- src/Repository/UserRepository.php | 6 +- .../SamlAuthenticationSuccessHandler.php | 4 +- src/Timesheet/TimesheetStatisticService.php | 8 +- .../DailyWorkingTimeChartProvider.php | 206 ------------------ src/Widget/Type/DailyWorkingTimeChart.php | 132 ++++++++++- templates/base.html.twig | 2 - templates/user/2fa.html.twig | 43 +--- templates/user/api-token.html.twig | 29 +-- .../Configuration/SystemConfigurationTest.php | 9 - tests/Controller/Auth/SamlControllerTest.php | 4 +- tests/Entity/InvoiceTemplateTest.php | 1 - .../CellFormatter/AbstractFormatterTest.php | 3 +- .../CellFormatter/DateFormatterTest.php | 2 +- .../InvoiceItemDefaultHydratorTest.php | 1 + .../InvoiceModelActivityHydratorTest.php | 4 + .../InvoiceModelProjectHydratorTest.php | 4 + tests/Invoice/Renderer/DebugRendererTest.php | 9 + tests/Invoice/Renderer/PdfRendererTest.php | 6 +- tests/Invoice/ServiceInvoiceTest.php | 4 +- tests/Twig/LocaleFormatExtensionsTest.php | 2 +- .../Widget/Type/DailyWorkingTimeChartTest.php | 57 +---- tests/phpstan.neon | 11 +- translations/messages.de.xlf | 8 + translations/messages.en.xlf | 8 + 56 files changed, 441 insertions(+), 575 deletions(-) delete mode 100644 src/Widget/DataProvider/DailyWorkingTimeChartProvider.php diff --git a/composer.json b/composer.json index f336b3fc..8e5f12bb 100644 --- a/composer.json +++ b/composer.json @@ -84,6 +84,7 @@ "fakerphp/faker": "^1.15", "friendsofphp/php-cs-fixer": "^3.3", "phpstan/phpstan": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-doctrine": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", diff --git a/composer.lock b/composer.lock index 59dde827..fedab928 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "24f571304052968a6a7f49cac1109c5b", + "content-hash": "e857da36ba58dbba786a711ffe4f6320", "packages": [ { "name": "azuyalabs/yasumi", @@ -2374,16 +2374,16 @@ }, { "name": "kevinpapst/tabler-bundle", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/kevinpapst/TablerBundle.git", - "reference": "b4519deb2cb9fff3493c108c73a13af54401b5e8" + "reference": "d957405e18589a351856e4b7b8670731a5c34896" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/b4519deb2cb9fff3493c108c73a13af54401b5e8", - "reference": "b4519deb2cb9fff3493c108c73a13af54401b5e8", + "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/d957405e18589a351856e4b7b8670731a5c34896", + "reference": "d957405e18589a351856e4b7b8670731a5c34896", "shasum": "" }, "require": { @@ -2431,7 +2431,7 @@ "description": "Admin/Backend theme bundle for Symfony based on Tabler.io", "support": { "issues": "https://github.com/kevinpapst/TablerBundle/issues", - "source": "https://github.com/kevinpapst/TablerBundle/tree/1.3.0" + "source": "https://github.com/kevinpapst/TablerBundle/tree/1.4.0" }, "funding": [ { @@ -2443,7 +2443,7 @@ "type": "github" } ], - "time": "2024-02-12T16:13:20+00:00" + "time": "2024-04-12T10:09:29+00:00" }, { "name": "league/csv", @@ -3110,16 +3110,16 @@ }, { "name": "nelmio/api-doc-bundle", - "version": "v4.25.1", + "version": "4.25.2", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "f594e600bf5943008562be35c4acd60957434d22" + "reference": "fc5d60d74b63806f7d6bb80b8393db1a12ae2dbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/f594e600bf5943008562be35c4acd60957434d22", - "reference": "f594e600bf5943008562be35c4acd60957434d22", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/fc5d60d74b63806f7d6bb80b8393db1a12ae2dbc", + "reference": "fc5d60d74b63806f7d6bb80b8393db1a12ae2dbc", "shasum": "" }, "require": { @@ -3153,8 +3153,11 @@ "friendsofsymfony/rest-bundle": "^2.8|^3.0", "jms/serializer": "^1.14|^3.0", "jms/serializer-bundle": "^2.3|^3.0|^4.0|^5.0", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpstan/phpstan-symfony": "^1.3", "phpunit/phpunit": "^8.5|^9.6", - "sensio/framework-extra-bundle": "^5.4|^6.0", "symfony/asset": "^5.4|^6.0|^7.0", "symfony/browser-kit": "^5.4|^6.0|^7.0", "symfony/cache": "^5.4|^6.0|^7.0", @@ -3216,9 +3219,9 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.25.1" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/4.25.2" }, - "time": "2024-04-04T08:16:08+00:00" + "time": "2024-04-06T12:08:11+00:00" }, { "name": "nelmio/cors-bundle", @@ -3613,28 +3616,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", + "version": "5.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + "reference": "298d2febfe79d03fe714eb871d5538da55205b1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/298d2febfe79d03fe714eb871d5538da55205b1a", + "reference": "298d2febfe79d03fe714eb871d5538da55205b1a", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.1", "ext-filter": "*", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7", "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" + "mockery/mockery": "~1.3.5", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^5.13" }, "type": "library", "extra": { @@ -3658,15 +3668,15 @@ }, { "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "email": "opensource@ijaap.nl" } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.0" }, - "time": "2021-10-19T17:43:47+00:00" + "time": "2024-04-09T21:13:58+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -10769,16 +10779,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.52.1", + "version": "v3.53.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc" + "reference": "69a19093a9ded8d1baac62ed6c009b8bc148d008" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/6e77207f0d851862ceeb6da63e6e22c01b1587bc", - "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/69a19093a9ded8d1baac62ed6c009b8bc148d008", + "reference": "69a19093a9ded8d1baac62ed6c009b8bc148d008", "shasum": "" }, "require": { @@ -10802,6 +10812,7 @@ }, "require-dev": { "facile-it/paraunit": "^1.3 || ^2.0", + "infection/infection": "^0.27.11", "justinrainbow/json-schema": "^5.2", "keradus/cli-executor": "^2.1", "mikey179/vfsstream": "^1.6.11", @@ -10849,7 +10860,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.52.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.53.0" }, "funding": [ { @@ -10857,7 +10868,7 @@ "type": "github" } ], - "time": "2024-03-19T21:02:43+00:00" + "time": "2024-04-08T15:03:00+00:00" }, { "name": "masterminds/html5", @@ -11164,6 +11175,54 @@ ], "time": "2024-03-28T16:17:31+00:00" }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa", + "reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10.3" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-php-parser": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.1.4" + }, + "time": "2023-08-05T09:02:04+00:00" + }, { "name": "phpstan/phpstan-doctrine", "version": "1.3.65", @@ -11290,21 +11349,21 @@ }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.5.2", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542" + "reference": "568210bd301f94a0d4b1e5a0808c374c1b9cf11b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/7a50e9662ee9f3942e4aaaf3d603653f60282542", - "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/568210bd301f94a0d4b1e5a0808c374c1b9cf11b", + "reference": "568210bd301f94a0d4b1e5a0808c374c1b9cf11b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.34" + "phpstan/phpstan": "^1.10.60" }, "require-dev": { "nikic/php-parser": "^4.13.0", @@ -11333,22 +11392,22 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.2" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.3" }, - "time": "2023-10-30T14:35:06+00:00" + "time": "2024-04-06T07:43:25+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.3.9", + "version": "1.3.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "a32bc86da24495025d7aafd1ba62444d4a364a98" + "reference": "3fbf634b2495adaa1dc4b7300293f8abf74128d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/a32bc86da24495025d7aafd1ba62444d4a364a98", - "reference": "a32bc86da24495025d7aafd1ba62444d4a364a98", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/3fbf634b2495adaa1dc4b7300293f8abf74128d8", + "reference": "3fbf634b2495adaa1dc4b7300293f8abf74128d8", "shasum": "" }, "require": { @@ -11405,9 +11464,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.9" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.11" }, - "time": "2024-03-16T16:50:20+00:00" + "time": "2024-04-12T07:42:40+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index c8ec072f..6c8ae96a 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -23,6 +23,8 @@ doctrine: types: datetime: App\Doctrine\UTCDateTimeType orm: + controller_resolver: + auto_mapping: true auto_generate_proxy_classes: '%kernel.debug%' default_entity_manager: default enable_lazy_ghost_objects: true diff --git a/config/packages/security.yaml b/config/packages/security.yaml index f236d88b..03c9cdf1 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -20,6 +20,7 @@ security: api: access_token: token_handler: App\API\Authentication\AccessTokenHandler + remember_me: false request_matcher: App\API\Authentication\ApiRequestMatcher user_checker: App\Security\UserChecker stateless: true @@ -92,7 +93,7 @@ security: - { path: '^/(%app_locales%)/register', role: PUBLIC_ACCESS } - { path: '^/(%app_locales%)/resetting', role: PUBLIC_ACCESS } - { path: '^/(%app_locales%)/', roles: ROLE_USER } - - { path: '^/api', roles: IS_AUTHENTICATED_REMEMBERED } + - { path: '^/api', roles: IS_AUTHENTICATED } when@test: # this configuration simplifies testing URLs protected by the security mechanism diff --git a/phpstan.neon b/phpstan.neon index 28451043..d1ec845a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,6 +4,7 @@ includes: - %rootDir%/../phpstan-doctrine/extension.neon - %rootDir%/../phpstan-doctrine/rules.neon - %rootDir%/../phpstan-strict-rules/rules.neon + - %rootDir%/../phpstan-deprecation-rules/rules.neon - %rootDir%/../phpstan/conf/bleedingEdge.neon parameters: @@ -3447,11 +3448,6 @@ parameters: count: 1 path: src/Invoice/Calculator/AbstractMergedCalculator.php - - - message: "#^Cannot call method format\\(\\) on DateTime\\|null\\.$#" - count: 2 - path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - message: "#^Cannot call method getCurrency\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" count: 1 @@ -3462,26 +3458,11 @@ parameters: count: 1 path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - - message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#" - count: 2 - path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceItemDefaultHydrator\\:\\:hydrate\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - - message: "#^Parameter \\#1 \\$date of method App\\\\Invoice\\\\InvoiceFormatter\\:\\:getFormattedDateTime\\(\\) expects DateTimeInterface, DateTime\\|null given\\.$#" - count: 3 - path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - - - message: "#^Parameter \\#1 \\$date of method App\\\\Invoice\\\\InvoiceFormatter\\:\\:getFormattedTime\\(\\) expects DateTimeInterface, DateTime\\|null given\\.$#" - count: 2 - path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceModelActivityHydrator\\:\\:getBudgetValues\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -5572,95 +5553,15 @@ parameters: count: 1 path: src/Voter/TimesheetVoter.php - - - message: "#^Cannot access offset 'billable' on mixed\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot access offset 'day' on mixed\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot access offset 'details' on mixed\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot access offset 'duration' on mixed\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot access offset 'month' on mixed\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot access offset 'rate' on mixed\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot access offset 'year' on mixed\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - message: "#^Cannot call method getCustomer\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#" count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Activity\\|null\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php + path: src/Widget/Type/DailyWorkingTimeChart.php - message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot cast mixed to float\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Parameter \\#1 \\$details of method App\\\\Model\\\\Statistic\\\\Day\\:\\:setDetails\\(\\) expects array, mixed given\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Parameter \\#1 \\$seconds of method App\\\\Model\\\\Statistic\\\\Day\\:\\:setTotalDurationBillable\\(\\) expects int, mixed given\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Parameter \\#1 \\$year of method DateTime\\:\\:setDate\\(\\) expects int, mixed given\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Parameter \\#2 \\$month of method DateTime\\:\\:setDate\\(\\) expects int, mixed given\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php - - - - message: "#^Parameter \\#3 \\$day of method DateTime\\:\\:setDate\\(\\) expects int, mixed given\\.$#" - count: 1 - path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php + path: src/Widget/Type/DailyWorkingTimeChart.php - message: "#^Method App\\\\Widget\\\\Type\\\\AbstractWidget\\:\\:getUser\\(\\) should return App\\\\Entity\\\\User but returns App\\\\Entity\\\\User\\|null\\.$#" diff --git a/public/.htaccess b/public/.htaccess index 416790f4..74677b9b 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -1,3 +1,5 @@ +# See https://github.com/symfony/recipes-contrib/blob/main/symfony/apache-pack/1.0/public/.htaccess#L38 + # Use the front controller as index file. It serves as a fallback solution when # every other rewrite/redirect fails (e.g. in an aliased environment without # mod_rewrite). Additionally, this reduces the matching process for the @@ -5,30 +7,41 @@ # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). DirectoryIndex index.php -# Do NOT use Kimai with http, this is a huge security risk! -# Activate these lines if you configured SSL and certificates: +# By default, Apache does not evaluate symbolic links if you did not enable this +# feature in your server configuration. Uncomment the following line if you +# install assets as symlinks or if you experience problems related to symlinks +# when compiling LESS/Sass/CoffeScript assets. +# Options +SymLinksIfOwnerMatch -# -# RewriteEngine On -# RewriteCond %{SERVER_PORT} 80 -# RewriteRule ^(.*)$ https://kimai.example.com/$1 [R,L] -# +# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve +# to the front controller "/index.php" but be rewritten to "/index.php/index". + + Options -MultiViews + + # This Option needs to be enabled for RewriteRule, otherwise it will show an error like + # 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden' + Options +SymLinksIfOwnerMatch + RewriteEngine On # Determine the RewriteBase automatically and set it as environment variable. # If you are using Apache aliases to do mass virtual hosting or installed the # project in a subdirectory, the base path will be prepended to allow proper - # resolution of the app.php file and to redirect to the correct URI. It will + # resolution of the index.php file and to redirect to the correct URI. It will # work in environments without path prefix as well, providing a safe, one-size # fits all solution. But as you do not need it in this case, you can comment # the following 2 lines to eliminate the overhead. - RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ - RewriteRule ^(.*) - [E=BASE:%1] + RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$ + RewriteRule .* - [E=BASE:%1] + + # Sets the HTTP_AUTHORIZATION header removed by Apache + RewriteCond %{HTTP:Authorization} .+ + RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] # Redirect to URI without front controller to prevent duplicate content - # (with and without `/app.php`). Only do this redirect on the initial + # (with and without `/index.php`). Only do this redirect on the initial # rewrite by Apache and not on subsequent cycles. Otherwise we would get an # endless redirect loop (request -> rewrite to front controller -> # redirect -> request -> ...). @@ -38,16 +51,14 @@ DirectoryIndex index.php # - disable this feature by commenting the following 2 lines or # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the # following RewriteCond (best solution) - RewriteCond %{ENV:REDIRECT_STATUS} ^$ - RewriteRule ^index\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L] + RewriteCond %{ENV:REDIRECT_STATUS} ="" + RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] # If the requested filename exists, simply serve it. # We only want to let Apache serve files and not directories. - RewriteCond %{REQUEST_FILENAME} -f - RewriteRule .? - [L] - # Rewrite all other queries to the front controller. - RewriteRule .? %{ENV:BASE}/index.php [L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ %{ENV:BASE}/index.php [L] @@ -55,7 +66,7 @@ DirectoryIndex index.php # When mod_rewrite is not available, we instruct a temporary redirect of # the start page to the front controller explicitly so that the website # and the generated links can still be used. - RedirectMatch 302 ^/$ /index.php/ + RedirectMatch 307 ^/$ /index.php/ # RedirectTemp cannot be used instead - + \ No newline at end of file diff --git a/src/API/ActionsController.php b/src/API/ActionsController.php index be72d9a7..5f5bc955 100644 --- a/src/API/ActionsController.php +++ b/src/API/ActionsController.php @@ -26,7 +26,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Translation\TranslatorInterface; #[Route(path: '/actions')] -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'Actions')] final class ActionsController extends BaseApiController { diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php index 9851b87e..0ba5f52f 100644 --- a/src/API/ActivityController.php +++ b/src/API/ActivityController.php @@ -33,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/activities')] -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'Activity')] final class ActivityController extends BaseApiController { diff --git a/src/API/ConfigurationController.php b/src/API/ConfigurationController.php index 83262c73..44cb469c 100644 --- a/src/API/ConfigurationController.php +++ b/src/API/ConfigurationController.php @@ -19,7 +19,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'Default')] final class ConfigurationController extends BaseApiController { diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php index b02c7a08..c4c3fcd2 100644 --- a/src/API/CustomerController.php +++ b/src/API/CustomerController.php @@ -33,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/customers')] -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'Customer')] final class CustomerController extends BaseApiController { diff --git a/src/API/ProjectController.php b/src/API/ProjectController.php index 1533862f..3cc853d8 100644 --- a/src/API/ProjectController.php +++ b/src/API/ProjectController.php @@ -35,7 +35,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Validator\Constraints; #[Route(path: '/projects')] -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'Project')] final class ProjectController extends BaseApiController { diff --git a/src/API/StatusController.php b/src/API/StatusController.php index 0da69f86..a34a1c19 100644 --- a/src/API/StatusController.php +++ b/src/API/StatusController.php @@ -20,7 +20,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'Default')] final class StatusController extends BaseApiController { diff --git a/src/API/TagController.php b/src/API/TagController.php index 92b25676..b3eace3f 100644 --- a/src/API/TagController.php +++ b/src/API/TagController.php @@ -23,7 +23,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/tags')] -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'Tag')] final class TagController extends BaseApiController { diff --git a/src/API/TeamController.php b/src/API/TeamController.php index a066a5a4..ed2fa3d7 100644 --- a/src/API/TeamController.php +++ b/src/API/TeamController.php @@ -30,7 +30,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/teams')] -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'Team')] final class TeamController extends BaseApiController { diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index 168de584..7a9f9606 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -42,7 +42,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Validator\Constraints; #[Route(path: '/timesheets')] -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'Timesheet')] final class TimesheetController extends BaseApiController { @@ -85,14 +85,14 @@ final class TimesheetController extends BaseApiController #[Rest\QueryParam(name: 'tags', map: true, strict: true, nullable: true, default: [], description: 'List of tag names, e.g. tags[]=bar&tags[]=foo')] #[Rest\QueryParam(name: 'orderBy', requirements: 'id|begin|end|rate', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)')] #[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: DESC)')] - #[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5)')] - #[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5)')] + #[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')] + #[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')] #[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')] #[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')] #[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')] #[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')] #[Rest\QueryParam(name: 'term', description: 'Free search term')] - #[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5). Available since Kimai 1.10 and works only for records that were created/updated since then.')] + #[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')] public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response { $query = new TimesheetQuery(false); @@ -398,7 +398,7 @@ final class TimesheetController extends BaseApiController #[IsGranted('view_own_timesheet')] #[OA\Response(response: 200, description: 'Returns the collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))] #[Route(methods: ['GET'], path: '/recent', name: 'recent_timesheet')] - #[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included. Default: today - 1 year (format: HTML5)')] + #[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included. Default: today - 1 year (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')] #[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries (default: 10)')] public function recentAction(ParamFetcherInterface $paramFetcher): Response { diff --git a/src/API/UserController.php b/src/API/UserController.php index 90a3c499..ce373162 100644 --- a/src/API/UserController.php +++ b/src/API/UserController.php @@ -33,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/users')] -#[IsGranted('IS_AUTHENTICATED_REMEMBERED')] +#[IsGranted('IS_AUTHENTICATED')] #[OA\Tag(name: 'User')] final class UserController extends BaseApiController { diff --git a/src/Configuration/SystemConfiguration.php b/src/Configuration/SystemConfiguration.php index 9bb7d79f..ea16ae5c 100644 --- a/src/Configuration/SystemConfiguration.php +++ b/src/Configuration/SystemConfiguration.php @@ -127,6 +127,8 @@ final class SystemConfiguration */ public function offsetExists($offset): bool { + @trigger_error('The method "SystemConfiguration::offsetExists()" is deprecated, use "has()" instead', E_USER_DEPRECATED); + return $this->has($offset); } @@ -135,6 +137,8 @@ final class SystemConfiguration */ public function offsetGet($offset): mixed { + @trigger_error('The method "SystemConfiguration::offsetGet()" is deprecated, use "find()" instead', E_USER_DEPRECATED); + return $this->find($offset); } @@ -143,16 +147,9 @@ final class SystemConfiguration */ public function offsetSet(mixed $offset, mixed $value): void { - $this->set($offset, $value); - } + @trigger_error('The method "SystemConfiguration::offsetSet()" is deprecated, use "set()" instead', E_USER_DEPRECATED); - /** - * @deprecated since 2.0.35 - * @throws \BadMethodCallException - */ - public function offsetUnset(mixed $offset): void - { - throw new \BadMethodCallException('SystemBundleConfiguration does not support offsetUnset()'); + $this->set($offset, $value); } // ========== Authentication configurations ========== diff --git a/src/Constants.php b/src/Constants.php index 8338735d..b88db0c4 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ class Constants /** * The current release version */ - public const VERSION = '2.14.0'; + public const VERSION = '2.15.0'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 21400; + public const VERSION_ID = 21500; /** * The software name */ diff --git a/src/Controller/AbstractController.php b/src/Controller/AbstractController.php index 2ccbdc26..1985b2a0 100644 --- a/src/Controller/AbstractController.php +++ b/src/Controller/AbstractController.php @@ -199,7 +199,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv } } - protected function logException(\Exception $ex): void + protected function logException(\Throwable $ex): void { $this->container->get('logger')->critical($ex->getMessage()); } diff --git a/src/Controller/DoctorController.php b/src/Controller/DoctorController.php index ee0f0a2e..601d783a 100644 --- a/src/Controller/DoctorController.php +++ b/src/Controller/DoctorController.php @@ -265,6 +265,7 @@ final class DoctorController extends AbstractController 'sys_temp_dir', 'date.timezone', 'session.gc_maxlifetime', + 'disable_functions' ]; $settings = []; diff --git a/src/Controller/PluginController.php b/src/Controller/PluginController.php index 5cbe10f8..790ceca8 100644 --- a/src/Controller/PluginController.php +++ b/src/Controller/PluginController.php @@ -61,7 +61,7 @@ final class PluginController extends AbstractController $item->expiresAfter(86400); // one day return $response->toArray(); - } catch (\Exception $exception) { + } catch (\Throwable $exception) { $this->logException($exception); $this->flashError('Could not download plugin information'); } diff --git a/src/Controller/TimesheetAbstractController.php b/src/Controller/TimesheetAbstractController.php index 82b818ec..02514f6b 100644 --- a/src/Controller/TimesheetAbstractController.php +++ b/src/Controller/TimesheetAbstractController.php @@ -88,12 +88,12 @@ abstract class TimesheetAbstractController extends AbstractController if ($canSeeRate) { $table->addColumn('hourlyRate', ['class' => 'text-end d-none text-nowrap']); - $table->addColumn('internalRate', ['class' => 'text-end text-nowrap d-none d-md-table-cell']); + $table->addColumn('internalRate', ['class' => 'text-end text-nowrap d-none d-xxl-table-cell']); $table->addColumn('rate', ['class' => 'text-end text-nowrap']); } $table->addColumn('customer', ['class' => 'd-none d-md-table-cell']); - $table->addColumn('project', ['class' => 'd-none d-lg-table-cell']); + $table->addColumn('project', ['class' => 'd-none d-xl-table-cell']); $table->addColumn('activity', ['class' => 'd-none d-xl-table-cell']); $table->addColumn('description', ['class' => 'd-none']); $table->addColumn('tags', ['class' => 'd-none badges', 'orderBy' => false]); @@ -103,7 +103,7 @@ abstract class TimesheetAbstractController extends AbstractController } if ($canSeeUsername) { - $table->addColumn('username', ['class' => 'd-none d-sm-table-cell', 'orderBy' => false]); + $table->addColumn('username', ['class' => 'd-none d-md-table-cell', 'orderBy' => false]); } $table->addColumn('billable', ['class' => 'text-center d-none w-min', 'orderBy' => false]); diff --git a/src/Doctrine/Extensions/Date.php b/src/Doctrine/Extensions/Date.php index 28964bf7..813b1139 100644 --- a/src/Doctrine/Extensions/Date.php +++ b/src/Doctrine/Extensions/Date.php @@ -11,9 +11,9 @@ namespace App\Doctrine\Extensions; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\TokenType; final class Date extends FunctionNode { @@ -26,9 +26,9 @@ final class Date extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); $this->value = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Doctrine/Extensions/Day.php b/src/Doctrine/Extensions/Day.php index 59d9770a..f7790384 100644 --- a/src/Doctrine/Extensions/Day.php +++ b/src/Doctrine/Extensions/Day.php @@ -11,9 +11,9 @@ namespace App\Doctrine\Extensions; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\TokenType; final class Day extends FunctionNode { @@ -26,9 +26,9 @@ final class Day extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); $this->value = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Doctrine/Extensions/Month.php b/src/Doctrine/Extensions/Month.php index ba39e29b..b74c5925 100644 --- a/src/Doctrine/Extensions/Month.php +++ b/src/Doctrine/Extensions/Month.php @@ -11,9 +11,9 @@ namespace App\Doctrine\Extensions; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\TokenType; final class Month extends FunctionNode { @@ -26,9 +26,9 @@ final class Month extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); $this->value = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Doctrine/Extensions/Year.php b/src/Doctrine/Extensions/Year.php index 540d2c88..c107cca6 100644 --- a/src/Doctrine/Extensions/Year.php +++ b/src/Doctrine/Extensions/Year.php @@ -11,9 +11,9 @@ namespace App\Doctrine\Extensions; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\TokenType; final class Year extends FunctionNode { @@ -26,9 +26,9 @@ final class Year extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); $this->value = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Doctrine/UTCDateTimeType.php b/src/Doctrine/UTCDateTimeType.php index c6c644c1..87d8066b 100644 --- a/src/Doctrine/UTCDateTimeType.php +++ b/src/Doctrine/UTCDateTimeType.php @@ -12,6 +12,7 @@ namespace App\Doctrine; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\DateTimeType; +use Doctrine\DBAL\Types\Types; final class UTCDateTimeType extends DateTimeType { @@ -67,7 +68,7 @@ final class UTCDateTimeType extends DateTimeType if (!$converted) { throw ConversionException::conversionFailedFormat( $value, - $this->getName(), + Types::DATETIME_MUTABLE, $platform->getDateTimeFormatString() ); } diff --git a/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php index 1dd3d5a3..d4162def 100644 --- a/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php @@ -15,6 +15,8 @@ use App\Invoice\InvoiceModel; final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator { + private const DATE_PROCESS_FORMAT = 'Y-m-d h:i:s'; + private InvoiceModel $model; public function setInvoiceModel(InvoiceModel $model): void @@ -75,17 +77,24 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator 'entry.duration_format' => $formatter->getFormattedDuration($item->getDuration()), 'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($item->getDuration()), 'entry.duration_minutes' => (int) ($item->getDuration() / 60), - 'entry.begin' => $formatter->getFormattedDateTime($begin), - 'entry.begin_time' => $formatter->getFormattedTime($begin), - 'entry.begin_timestamp' => $begin->getTimestamp(), - 'entry.end' => $formatter->getFormattedDateTime($end), - 'entry.end_time' => $formatter->getFormattedTime($end), - 'entry.end_timestamp' => $end->getTimestamp(), - 'entry.date' => $formatter->getFormattedDateTime($begin), - 'entry.week' => \intval($begin->format('W')), - 'entry.weekyear' => $begin->format('o'), ]; + if ($begin !== null) { + $values['entry.begin'] = $formatter->getFormattedDateTime($begin); + $values['entry.begin_time'] = $formatter->getFormattedTime($begin); + $values['entry.begin_timestamp'] = $begin->getTimestamp(); + $values['entry.date'] = $formatter->getFormattedDateTime($begin); + $values['entry.date_process'] = $begin->format(self::DATE_PROCESS_FORMAT); // since 2.14 + $values['entry.week'] = \intval($begin->format('W')); + $values['entry.weekyear'] = $begin->format('o'); + } + + if ($end !== null) { + $values['entry.end'] = $formatter->getFormattedDateTime($end); + $values['entry.end_time'] = $formatter->getFormattedTime($end); + $values['entry.end_timestamp'] = $end->getTimestamp(); + } + if (null !== $user) { $values = array_merge($values, [ 'entry.user_id' => $user->getId(), diff --git a/src/Invoice/Hydrator/InvoiceModelActivityHydrator.php b/src/Invoice/Hydrator/InvoiceModelActivityHydrator.php index ade48933..d5fe0bf8 100644 --- a/src/Invoice/Hydrator/InvoiceModelActivityHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelActivityHydrator.php @@ -66,6 +66,8 @@ final class InvoiceModelActivityHydrator implements InvoiceModelHydrator $prefix . 'id' => $activity->getId(), $prefix . 'name' => $activity->getName() ?? '', $prefix . 'comment' => $activity->getComment() ?? '', + $prefix . 'number' => $activity->getNumber() ?? '', + $prefix . 'invoice_text' => $activity->getInvoiceText() ?? '', ]; if ($model->getQuery()?->getEnd() !== null) { diff --git a/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php b/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php index e0702a31..c5c871e4 100644 --- a/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php @@ -79,6 +79,8 @@ final class InvoiceModelProjectHydrator implements InvoiceModelHydrator $prefix . 'budget_time' => $project->getTimeBudget(), $prefix . 'budget_time_decimal' => $formatter->getFormattedDecimalDuration($project->getTimeBudget()), $prefix . 'budget_time_minutes' => (int) ($project->getTimeBudget() / 60), + $prefix . 'number' => $project->getNumber() ?? '', + $prefix . 'invoice_text' => $project->getInvoiceText() ?? '', ]; if ($model->getQuery()?->getEnd() !== null) { diff --git a/src/Repository/Query/ActivityQuery.php b/src/Repository/Query/ActivityQuery.php index 88cfa350..0d54b808 100644 --- a/src/Repository/Query/ActivityQuery.php +++ b/src/Repository/Query/ActivityQuery.php @@ -17,7 +17,14 @@ use App\Entity\Project; class ActivityQuery extends ProjectQuery { public const ACTIVITY_ORDER_ALLOWED = [ - 'name', 'description' => 'comment', 'customer', 'project', 'budget', 'timeBudget', 'visible' + 'name', + 'description' => 'comment', + 'activity_number' => 'number', + 'customer', + 'project', + 'budget', + 'timeBudget', + 'visible' ]; /** diff --git a/src/Repository/Query/ProjectQuery.php b/src/Repository/Query/ProjectQuery.php index 8b2788c3..baac1836 100644 --- a/src/Repository/Query/ProjectQuery.php +++ b/src/Repository/Query/ProjectQuery.php @@ -16,8 +16,17 @@ class ProjectQuery extends BaseQuery implements VisibilityInterface use VisibilityTrait; public const PROJECT_ORDER_ALLOWED = [ - 'name', 'description' => 'comment', 'customer', 'orderNumber', 'orderDate', - 'project_start', 'project_end', 'budget', 'timeBudget', 'visible' + 'name', + 'description' => 'comment', + 'project_number' => 'number', + 'customer', + 'orderNumber', + 'orderDate', + 'project_start', + 'project_end', + 'budget', + 'timeBudget', + 'visible' ]; /** diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 9789ba98..99994da0 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -62,7 +62,11 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void { - if (!($user instanceof User)) { + if (!($user instanceof User) || !$user->isInternalUser()) { + return; + } + + if ($user->getPassword() === $newHashedPassword) { return; } diff --git a/src/Saml/Security/SamlAuthenticationSuccessHandler.php b/src/Saml/Security/SamlAuthenticationSuccessHandler.php index 16e65428..bd9403ad 100644 --- a/src/Saml/Security/SamlAuthenticationSuccessHandler.php +++ b/src/Saml/Security/SamlAuthenticationSuccessHandler.php @@ -29,7 +29,9 @@ final class SamlAuthenticationSuccessHandler extends DefaultAuthenticationSucces } $relayState = $request->get('RelayState'); - if (null !== $relayState && $relayState !== $this->httpUtils->generateUri($request, $this->options['login_path'])) { + $loginUrl = $this->httpUtils->generateUri($request, $this->options['login_path']); + + if ($relayState !== null && $relayState !== '' && $relayState !== $loginUrl) { return $relayState; } diff --git a/src/Timesheet/TimesheetStatisticService.php b/src/Timesheet/TimesheetStatisticService.php index f62bc1a0..82db0ff2 100644 --- a/src/Timesheet/TimesheetStatisticService.php +++ b/src/Timesheet/TimesheetStatisticService.php @@ -52,9 +52,9 @@ final class TimesheetStatisticService ->addSelect('DAY(t.date) as day') ->addSelect('MONTH(t.date) as month') ->addSelect('YEAR(t.date) as year') - ->where($qb->expr()->isNotNull('t.end')) ->andWhere($qb->expr()->between('t.date', ':begin', ':end')) ->andWhere($qb->expr()->in('t.user', ':user')) + ->andWhere($qb->expr()->isNotNull('t.end')) ->setParameter('begin', $begin->format('Y-m-d')) ->setParameter('end', $end->format('Y-m-d')) ->setParameter('user', $users) @@ -124,9 +124,9 @@ final class TimesheetStatisticService ->addSelect('IDENTITY(t.project) as project') ->addSelect('IDENTITY(t.activity) as activity') ->addSelect('DATE(t.date) as date') - ->where($qb->expr()->isNotNull('t.end')) ->andWhere($qb->expr()->between('t.date', ':begin', ':end')) ->andWhere($qb->expr()->in('t.user', ':user')) + ->andWhere($qb->expr()->isNotNull('t.end')) ->setParameter('begin', $begin->format('Y-m-d')) ->setParameter('end', $end->format('Y-m-d')) ->setParameter('user', $users) @@ -200,9 +200,9 @@ final class TimesheetStatisticService ->addSelect('IDENTITY(t.activity) as activity') ->addSelect('YEAR(t.date) as year') ->addSelect('MONTH(t.date) as month') - ->where($qb->expr()->isNotNull('t.end')) ->andWhere($qb->expr()->between('t.date', ':begin', ':end')) ->andWhere($qb->expr()->in('t.user', ':user')) + ->andWhere($qb->expr()->isNotNull('t.end')) ->setParameter('begin', $begin->format('Y-m-d')) ->setParameter('end', $end->format('Y-m-d')) ->setParameter('user', $users) @@ -292,9 +292,9 @@ final class TimesheetStatisticService ->addSelect('MONTH(t.date) as month') ->addSelect('YEAR(t.date) as year') ->addSelect('IDENTITY(t.user) as user') - ->where($qb->expr()->isNotNull('t.end')) ->andWhere($qb->expr()->between('t.date', ':begin', ':end')) ->andWhere($qb->expr()->in('t.user', ':user')) + ->andWhere($qb->expr()->isNotNull('t.end')) ->setParameter('begin', $begin->format('Y-m-d')) ->setParameter('end', $end->format('Y-m-d')) ->setParameter('user', $users) diff --git a/src/Widget/DataProvider/DailyWorkingTimeChartProvider.php b/src/Widget/DataProvider/DailyWorkingTimeChartProvider.php deleted file mode 100644 index 8db972dd..00000000 --- a/src/Widget/DataProvider/DailyWorkingTimeChartProvider.php +++ /dev/null @@ -1,206 +0,0 @@ - - */ - protected function getDailyData(DateTimeInterface $begin, DateTimeInterface $end, ?User $user = null): array - { - $qb = $this->repository->createQueryBuilder('t'); - - $or = $qb->expr()->orX(); - $or->add($qb->expr()->between(':begin', 't.begin', 't.end')); - $or->add($qb->expr()->between(':end', 't.begin', 't.end')); - $or->add($qb->expr()->between('t.begin', ':begin', ':end')); - $or->add($qb->expr()->between('t.end', ':begin', ':end')); - - $qb->select('t, p, a, c') - ->andWhere($qb->expr()->isNotNull('t.end')) - ->andWhere($or) - ->orderBy('t.begin', 'DESC') - ->setParameter('begin', $begin) - ->setParameter('end', $end) - ->leftJoin('t.activity', 'a') - ->leftJoin('t.project', 'p') - ->leftJoin('p.customer', 'c') - ; - - if (null !== $user) { - $qb - ->andWhere($qb->expr()->eq('t.user', ':user')) - ->setParameter('user', $user) - ; - } - - $timesheets = $qb->getQuery()->getResult(); - - $results = []; - /** @var Timesheet $result */ - foreach ($timesheets as $result) { - /** @var DateTime $beginTmp */ - $beginTmp = $result->getBegin(); - /** @var DateTime $endTmp */ - $endTmp = $result->getEnd(); - $dateKeyEnd = $endTmp->format('Ymd'); - - do { - $dateKey = $beginTmp->format('Ymd'); - - if ($dateKey !== $dateKeyEnd) { - $newDateBegin = clone $beginTmp; - $newDateBegin->add(new \DateInterval('P1D')); - // overlapping records should always start at midnight - $newDateBegin->setTime(0, 0, 0); - } else { - $newDateBegin = clone $endTmp; - } - - // make sure to exclude entries that are outside the requested time-range: - // these entries can exist if you have long running entries that started before $begin - // for statistical reasons we have to include everything between $begin and $end while - // excluding everything that is outside of that range - // -------------------------------------------------------------------------------------- - // Be aware that this will NOT filter every record, in case there is a timezone mismatch between the - // begin/end dates and the ones from the database (eg. recorded in UTC) - which might actually be - // before $begin (which happens thanks to the timezone conversion when querying the database) - if ($newDateBegin > $begin && $beginTmp < $end) { - if (!isset($results[$dateKey])) { - $results[$dateKey] = [ - 'rate' => 0, - 'duration' => 0, - 'billable' => 0, // duration - 'month' => $beginTmp->format('n'), - 'year' => $beginTmp->format('Y'), - 'day' => $beginTmp->format('j'), - 'details' => [] - ]; - } - $duration = $newDateBegin->getTimestamp() - $beginTmp->getTimestamp(); - $durationPercent = 0; - if ($result->getDuration() !== null && $result->getDuration() > 0) { - $durationPercent = $duration / $result->getDuration(); - } - $rate = $result->getRate() * $durationPercent; - - $results[$dateKey]['rate'] += $rate; - $results[$dateKey]['duration'] += $duration; - if ($result->isBillable()) { - $results[$dateKey]['billable'] += $duration; - } - $detailsId = - $result->getProject()->getCustomer()->getId() - . '_' . $result->getProject()->getId() - . '_' . $result->getActivity()->getId() - ; - - if (!isset($results[$dateKey]['details'][$detailsId])) { - $results[$dateKey]['details'][$detailsId] = [ - 'project' => $result->getProject(), - 'activity' => $result->getActivity(), - 'duration' => 0, - 'rate' => 0, - 'billable' => 0, // duration - ]; - } - - $results[$dateKey]['details'][$detailsId]['duration'] += $duration; - $results[$dateKey]['details'][$detailsId]['rate'] += $rate; - if ($result->isBillable()) { - $results[$dateKey]['details'][$detailsId]['billable'] += $duration; - } - } - - $beginTmp = $newDateBegin; - - // yes, we only want to compare the day, not the time - if ((int) $end->format('Ymd') < (int) $newDateBegin->format('Ymd')) { - break; - } - } while ($dateKey !== $dateKeyEnd); - } - - ksort($results); - - foreach ($results as $key => $value) { - $results[$key]['details'] = array_values($results[$key]['details']); - } - - return array_values($results); - } - - /** - * @deprecated since 2.0 - use TimesheetStatisticService::getDailyStatistics() instead - * - * @param User|null $user - * @param DateTimeInterface $begin - * @param DateTimeInterface $end - * @return Day[] - * @throws \Exception - */ - public function getData(?User $user, DateTimeInterface $begin, DateTimeInterface $end): array - { - /** @var Day[] $days */ - $days = []; - - // prefill the array - $tmp = DateTime::createFromInterface($end); - $until = (int) $begin->format('Ymd'); - while ((int) $tmp->format('Ymd') >= $until) { - $last = clone $tmp; - $days[$last->format('Ymd')] = new Day($last, 0, 0.00); - $tmp->modify('-1 day'); - } - - $results = $this->getDailyData($begin, $end, $user); - - foreach ($results as $statRow) { - $dateTime = DateTime::createFromInterface($begin); - $dateTime->setDate($statRow['year'], $statRow['month'], $statRow['day']); - $dateTime->setTime(0, 0, 0); - $day = new Day($dateTime, (int) $statRow['duration'], (float) $statRow['rate']); - $day->setTotalDurationBillable($statRow['billable']); - $day->setDetails($statRow['details']); - $dateKey = $dateTime->format('Ymd'); - // make sure entries from other timezones are filtered - if (!\array_key_exists($dateKey, $days)) { - continue; - } - $days[$dateKey] = $day; - } - - ksort($days); - - return array_values($days); - } -} diff --git a/src/Widget/Type/DailyWorkingTimeChart.php b/src/Widget/Type/DailyWorkingTimeChart.php index 7f15c50b..0f1ce1c0 100644 --- a/src/Widget/Type/DailyWorkingTimeChart.php +++ b/src/Widget/Type/DailyWorkingTimeChart.php @@ -11,17 +11,21 @@ namespace App\Widget\Type; use App\Entity\Activity; use App\Entity\Project; +use App\Entity\Timesheet; +use App\Entity\User; +use App\Model\Statistic\Day; +use App\Repository\TimesheetRepository; use App\Timesheet\DateTimeFactory; -use App\Widget\DataProvider\DailyWorkingTimeChartProvider; use App\Widget\WidgetInterface; use DateTime; +use DateTimeInterface; /** * This is rendered inside the PaginatedWorkingTimeChart. */ final class DailyWorkingTimeChart extends AbstractWidget { - public function __construct(private DailyWorkingTimeChartProvider $dailyWorkingTimeChartProvider) + public function __construct(private readonly TimesheetRepository $repository) { } @@ -47,7 +51,7 @@ final class DailyWorkingTimeChart extends AbstractWidget /** * @param array> $options - @return array> + * @return array> */ public function getOptions(array $options = []): array { @@ -71,7 +75,7 @@ final class DailyWorkingTimeChart extends AbstractWidget $begin = $options['begin']; if (!($begin instanceof \DateTimeInterface)) { if (\is_string($begin)) { - $begin = new DateTime($begin, new \DateTimeZone($user->getTimezone())); + $begin = new \DateTimeImmutable($begin, new \DateTimeZone($user->getTimezone())); } else { $begin = $dateTimeFactory->getStartOfWeek(); } @@ -80,14 +84,14 @@ final class DailyWorkingTimeChart extends AbstractWidget $end = $options['end']; if (!($end instanceof \DateTimeInterface)) { if (\is_string($end)) { - $end = new DateTime($end, new \DateTimeZone($user->getTimezone())); + $end = new \DateTimeImmutable($end, new \DateTimeZone($user->getTimezone())); } else { $end = $dateTimeFactory->getEndOfWeek($begin); } } $activities = []; - $statistics = $this->dailyWorkingTimeChartProvider->getData($user, $begin, $end); + $statistics = $this->getPreparedData($user, $begin, $end); foreach ($statistics as $day) { foreach ($day->getDetails() as $entry) { @@ -125,4 +129,120 @@ final class DailyWorkingTimeChart extends AbstractWidget { return 'widget/widget-dailyworkingtimechart.html.twig'; } + + /** + * @return list>}> + */ + private function getDailyData(DateTimeInterface $begin, DateTimeInterface $end, User $user): array + { + $qb = $this->repository->createQueryBuilder('t'); + + $qb->select('t, p, a, c') + ->andWhere($qb->expr()->between('t.date', ':begin', ':end')) + ->andWhere($qb->expr()->eq('t.user', ':user')) + ->andWhere($qb->expr()->isNotNull('t.end')) + ->setParameter('begin', $begin->format('Y-m-d')) + ->setParameter('end', $end->format('Y-m-d')) + ->setParameter('user', $user) + ->leftJoin('t.activity', 'a') + ->leftJoin('t.project', 'p') + ->leftJoin('p.customer', 'c') + ; + + $timesheets = $qb->getQuery()->getResult(); + + $results = []; + /** @var Timesheet $result */ + foreach ($timesheets as $result) { + /** @var DateTime $beginTmp */ + $beginTmp = $result->getBegin(); + + $dateKey = $beginTmp->format('Ymd'); + + if (!isset($results[$dateKey])) { + $results[$dateKey] = [ + 'duration' => 0, + 'billable' => 0, // duration + 'month' => $beginTmp->format('n'), + 'year' => $beginTmp->format('Y'), + 'day' => $beginTmp->format('j'), + 'details' => [] + ]; + } + $duration = $result->getDuration() ?? 0; + + $results[$dateKey]['duration'] += $duration; + if ($result->isBillable()) { + $results[$dateKey]['billable'] += $duration; + } + $detailsId = + $result->getProject()->getCustomer()->getId() + . '_' . ($result->getProject()?->getId() ?? '') + . '_' . ($result->getActivity()?->getId() ?? '') + ; + + if (!isset($results[$dateKey]['details'][$detailsId])) { + $results[$dateKey]['details'][$detailsId] = [ + 'project' => $result->getProject(), + 'activity' => $result->getActivity(), + 'duration' => 0, + 'billable' => 0, // duration + ]; + } + + $results[$dateKey]['details'][$detailsId]['duration'] += $duration; + if ($result->isBillable()) { + $results[$dateKey]['details'][$detailsId]['billable'] += $duration; + } + } + + ksort($results); + + foreach ($results as $key => $value) { + $results[$key]['details'] = array_values($value['details']); + } + + return array_values($results); + } + + /** + * @return Day[] + * @throws \Exception + */ + private function getPreparedData(User $user, DateTimeInterface $begin, DateTimeInterface $end): array + { + /** @var Day[] $days */ + $days = []; + + // prefill the array + $tmp = DateTime::createFromInterface($end); + $until = (int) $begin->format('Ymd'); + while ((int) $tmp->format('Ymd') >= $until) { + $last = clone $tmp; + $days[$last->format('Ymd')] = new Day($last, 0, 0.00); + $tmp->modify('-1 day'); + } + + // TODO replace with TimesheetStatisticService::getDailyStatistics() + $results = $this->getDailyData($begin, $end, $user); + + foreach ($results as $statRow) { + $dateTime = DateTime::createFromInterface($begin); + $dateTime->setDate((int) $statRow['year'], (int) $statRow['month'], (int) $statRow['day']); + $dateTime->setTime(0, 0, 0); + $day = new Day($dateTime, (int) $statRow['duration'], 0.00); // rate is not used in frontend + $day->setTotalDurationBillable($statRow['billable']); + $day->setDetails($statRow['details']); + $dateKey = $dateTime->format('Ymd'); + // make sure entries from other timezones are filtered + if (!\array_key_exists($dateKey, $days)) { + continue; + } + $days[$dateKey] = $day; + } + + ksort($days); + + return array_values($days); + } } diff --git a/templates/base.html.twig b/templates/base.html.twig index 3944f403..b7faa0d6 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -85,8 +85,6 @@ {{- get_title() -}} {% endblock %} -{% block page_actions_class %}col-auto ms-auto d-print-none{% endblock %} - {% block footer %} {% if 'dashboard' in app.request.attributes.get('_route') %} {{ parent() }} diff --git a/templates/user/2fa.html.twig b/templates/user/2fa.html.twig index 6a430191..6d9b73d4 100644 --- a/templates/user/2fa.html.twig +++ b/templates/user/2fa.html.twig @@ -11,54 +11,35 @@ TOTP QR Code {{ secret }}

- {% if user.totpAuthenticationEnabled %} -
-
- - -
-
- {{ form_start(deactivate, {'attr': {'id': 'user_two_factor_form'}}) }} - {{ form_end(deactivate) }} - {% else %} + {% if not user.totpAuthenticationEnabled %} {{ form_start(form, {'attr': {'id': 'user_two_factor_form'}}) }}

{{ 'profile.2fa_confirmation'|trans }}

{{ form_widget(form) }} {{ form_end(form) }} + {% else %} + {{ 'activated'|trans }} {% endif %} {% endblock %} {% block javascripts %} {{ parent() }} + {% if not user.totpAuthenticationEnabled %} + {% endif %} {% endblock %} diff --git a/templates/user/api-token.html.twig b/templates/user/api-token.html.twig index 90ade444..78540822 100644 --- a/templates/user/api-token.html.twig +++ b/templates/user/api-token.html.twig @@ -5,20 +5,20 @@
-
-

- {{ 'api_password.intro'|trans }} -

-

- URL: {{ url('api.swagger_ui', {}, false)|replace({'/doc': ''}) }} -

-
+
+

+ {{ 'api_password.intro'|trans }} +

+

+ URL: {{ url('api.swagger_ui', {}, false)|replace({'/doc': ''}) }} +

+
- +
{% if created_token is not null %} @@ -39,7 +39,7 @@ {% endif %} {% if access_tokens|length > 0 %} - +
@@ -81,6 +81,7 @@ {% embed '@theme/embeds/collapsible.html.twig' with {id: 'activity_invoice_settings'} %} {% import "macros/widgets.html.twig" as widgets %} {% block title %}{{ 'password'|trans }}{% endblock %} + {% block box_class %}mt-3{% endblock %} {% block body %} {{ widgets.alert('danger', 'api_password_deprecated'|trans) }} {{ form_start(form) }} diff --git a/tests/Configuration/SystemConfigurationTest.php b/tests/Configuration/SystemConfigurationTest.php index d18c42ca..f2460ef6 100644 --- a/tests/Configuration/SystemConfigurationTest.php +++ b/tests/Configuration/SystemConfigurationTest.php @@ -163,15 +163,6 @@ class SystemConfigurationTest extends TestCase $this->assertEquals('2020-03-27', $sut->getFinancialYearStart()); } - public function testOffsetUnsetThrowsException(): void - { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('SystemBundleConfiguration does not support offsetUnset()'); - - $sut = $this->getSut($this->getDefaultSettings(), []); - $sut->offsetUnset('dfsdf'); - } - public function testUnknownConfigs(): void { $sut = $this->getSut($this->getDefaultSettings(), [ diff --git a/tests/Controller/Auth/SamlControllerTest.php b/tests/Controller/Auth/SamlControllerTest.php index 621b460f..36e2ab26 100644 --- a/tests/Controller/Auth/SamlControllerTest.php +++ b/tests/Controller/Auth/SamlControllerTest.php @@ -18,11 +18,11 @@ use App\Tests\Mocks\Saml\SamlAuthFactoryFactory; use App\Tests\Mocks\SystemConfigurationFactory; use OneLogin\Saml2\Auth; use PHPUnit\Framework\TestCase; -use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Security\Http\SecurityRequestAttributes; /** * @group integration @@ -127,7 +127,7 @@ class SamlControllerTest extends TestCase $request = new Request(); $request->setSession($this->createMock(SessionInterface::class)); - $request->attributes->set(Security::AUTHENTICATION_ERROR, new \Exception('My test error')); + $request->attributes->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, new \Exception('My test error')); $factory = $this->getMockBuilder(SamlAuthFactory::class)->disableOriginalConstructor()->getMock(); diff --git a/tests/Entity/InvoiceTemplateTest.php b/tests/Entity/InvoiceTemplateTest.php index e714601e..f57adeaa 100644 --- a/tests/Entity/InvoiceTemplateTest.php +++ b/tests/Entity/InvoiceTemplateTest.php @@ -35,7 +35,6 @@ class InvoiceTemplateTest extends TestCase self::assertEquals('default', $sut->getNumberGenerator()); self::assertEquals('default', $sut->getRenderer()); self::assertEquals(30, $sut->getDueDays()); - self::assertTrue($sut->isDecimalDuration()); } public function testSetNullForOptionalValues(): void diff --git a/tests/Export/Spreadsheet/CellFormatter/AbstractFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/AbstractFormatterTest.php index f331d8cc..2a512359 100644 --- a/tests/Export/Spreadsheet/CellFormatter/AbstractFormatterTest.php +++ b/tests/Export/Spreadsheet/CellFormatter/AbstractFormatterTest.php @@ -11,6 +11,7 @@ namespace App\Tests\Export\Spreadsheet\CellFormatter; use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\CellAddress; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Style; use PHPUnit\Framework\TestCase; @@ -42,7 +43,7 @@ abstract class AbstractFormatterTest extends TestCase $sut->setFormattedValue($worksheet, 1, 1, $this->getActualValue()); $cell = $worksheet->getCell([1, 1]); $this->assertCellValue($cell); - $this->assertCellStyle($worksheet->getStyleByColumnAndRow(1, 1)); + $this->assertCellStyle($worksheet->getStyle(CellAddress::fromColumnAndRow(1, 1))); } public function testSetNull(): void diff --git a/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php index e5cd754b..e5f29a24 100644 --- a/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php +++ b/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php @@ -52,6 +52,6 @@ class DateFormatterTest extends AbstractFormatterTest public function assertCellStyle(Style $style): void { - self::assertEquals(NumberFormat::FORMAT_DATE_YYYYMMDD2, $style->getNumberFormat()->getFormatCode()); + self::assertEquals(NumberFormat::FORMAT_DATE_YYYYMMDD, $style->getNumberFormat()->getFormatCode()); } } diff --git a/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php b/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php index bde73220..7afb2f31 100644 --- a/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php @@ -63,6 +63,7 @@ class InvoiceItemDefaultHydratorTest extends TestCase 'entry.end_time', 'entry.end_timestamp', 'entry.date', + 'entry.date_process', 'entry.week', 'entry.weekyear', 'entry.user_id', diff --git a/tests/Invoice/Hydrator/InvoiceModelActivityHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelActivityHydratorTest.php index f1b03dbd..18187eba 100644 --- a/tests/Invoice/Hydrator/InvoiceModelActivityHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceModelActivityHydratorTest.php @@ -37,6 +37,8 @@ class InvoiceModelActivityHydratorTest extends TestCase 'activity.id', 'activity.name', 'activity.comment', + 'activity.number', + 'activity.invoice_text', 'activity.meta.foo-activity', 'activity.budget_open', 'activity.budget_open_plain', @@ -45,6 +47,8 @@ class InvoiceModelActivityHydratorTest extends TestCase 'activity.1.id', 'activity.1.name', 'activity.1.comment', + 'activity.1.number', + 'activity.1.invoice_text', 'activity.1.meta.foo-activity', 'activity.1.budget_open', 'activity.1.budget_open_plain', diff --git a/tests/Invoice/Hydrator/InvoiceModelProjectHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelProjectHydratorTest.php index 09c09329..d0c9104d 100644 --- a/tests/Invoice/Hydrator/InvoiceModelProjectHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceModelProjectHydratorTest.php @@ -37,6 +37,8 @@ class InvoiceModelProjectHydratorTest extends TestCase 'project.id', 'project.name', 'project.comment', + 'project.number', + 'project.invoice_text', 'project.order_date', 'project.order_number', 'project.meta.foo-project', @@ -55,6 +57,8 @@ class InvoiceModelProjectHydratorTest extends TestCase 'project.1.id', 'project.1.name', 'project.1.comment', + 'project.1.number', + 'project.1.invoice_text', 'project.1.order_date', 'project.1.order_number', 'project.1.meta.foo-project', diff --git a/tests/Invoice/Renderer/DebugRendererTest.php b/tests/Invoice/Renderer/DebugRendererTest.php index a35755c5..e04fcf3d 100644 --- a/tests/Invoice/Renderer/DebugRendererTest.php +++ b/tests/Invoice/Renderer/DebugRendererTest.php @@ -152,6 +152,8 @@ class DebugRendererTest extends TestCase 'activity.id', 'activity.name', 'activity.comment', + 'activity.number', + 'activity.invoice_text', 'activity.meta.foo-activity', 'activity.budget_open', 'activity.budget_open_plain', @@ -184,6 +186,8 @@ class DebugRendererTest extends TestCase 'activity.1.id', 'activity.1.name', 'activity.1.comment', + 'activity.1.number', + 'activity.1.invoice_text', 'activity.1.meta.foo-activity', ]); } @@ -201,6 +205,8 @@ class DebugRendererTest extends TestCase 'project.id', 'project.name', 'project.comment', + 'project.number', + 'project.invoice_text', 'project.order_date', 'project.order_number', 'project.meta.foo-project', @@ -223,6 +229,8 @@ class DebugRendererTest extends TestCase 'project.1.id', 'project.1.name', 'project.1.comment', + 'project.1.number', + 'project.1.invoice_text', 'project.1.order_date', 'project.1.order_number', 'project.1.meta.foo-project', @@ -276,6 +284,7 @@ class DebugRendererTest extends TestCase 'entry.end_time', 'entry.end_timestamp', 'entry.date', + 'entry.date_process', 'entry.week', 'entry.weekyear', 'entry.user_id', diff --git a/tests/Invoice/Renderer/PdfRendererTest.php b/tests/Invoice/Renderer/PdfRendererTest.php index 5fd0d8e0..067f3b2c 100644 --- a/tests/Invoice/Renderer/PdfRendererTest.php +++ b/tests/Invoice/Renderer/PdfRendererTest.php @@ -116,9 +116,9 @@ class PdfRendererTest extends KernelTestCase $dirs = [ __DIR__ . '/../../../templates/invoice/renderer/', - __DIR__ . '/../../../var/invoices/', - __DIR__ . '/../../../var/invoices_customer/', - __DIR__ . '/../../../var/invoices_old/', + //__DIR__ . '/../../../var/invoices/', + //__DIR__ . '/../../../var/invoices_customer/', + //__DIR__ . '/../../../var/invoices_old/', ]; $files = []; diff --git a/tests/Invoice/ServiceInvoiceTest.php b/tests/Invoice/ServiceInvoiceTest.php index bb5ebc28..2801a3af 100644 --- a/tests/Invoice/ServiceInvoiceTest.php +++ b/tests/Invoice/ServiceInvoiceTest.php @@ -200,8 +200,8 @@ class ServiceInvoiceTest extends TestCase self::assertNotNull($query->getBegin()); self::assertNotNull($query->getEnd()); - self::assertEquals('2010-01-27T00:00:00+0100', $query->getBegin()->format(DATE_ISO8601)); - self::assertEquals('2020-11-27T23:59:59+0100', $query->getEnd()->format(DATE_ISO8601)); + self::assertEquals('2010-01-27T00:00:00+01:00', $query->getBegin()->format(DATE_ATOM)); + self::assertEquals('2020-11-27T23:59:59+01:00', $query->getEnd()->format(DATE_ATOM)); } public function testCreateModelsIncludesModelsWithNegativeTotal(): void diff --git a/tests/Twig/LocaleFormatExtensionsTest.php b/tests/Twig/LocaleFormatExtensionsTest.php index 281877c2..3866ed77 100644 --- a/tests/Twig/LocaleFormatExtensionsTest.php +++ b/tests/Twig/LocaleFormatExtensionsTest.php @@ -271,7 +271,7 @@ class LocaleFormatExtensionsTest extends TestCase $user->setTimezone('Asia/Dubai'); $date = $sut->createDate('2019-08-27 16:30:45', $user); - $this->assertEquals('2019-08-27T16:30:45+0400', $date->format(DATE_ISO8601)); + $this->assertEquals('2019-08-27T16:30:45+04:00', $date->format(DATE_ATOM)); $this->assertEquals('Asia/Dubai', $date->getTimezone()->getName()); $date = $sut->createDate('2019-08-27 16:30:45', null); diff --git a/tests/Widget/Type/DailyWorkingTimeChartTest.php b/tests/Widget/Type/DailyWorkingTimeChartTest.php index f3696cd6..c4cb7bae 100644 --- a/tests/Widget/Type/DailyWorkingTimeChartTest.php +++ b/tests/Widget/Type/DailyWorkingTimeChartTest.php @@ -9,11 +9,8 @@ namespace App\Tests\Widget\Type; -use App\Entity\Activity; -use App\Entity\Project; use App\Entity\User; -use App\Model\Statistic\Day; -use App\Widget\DataProvider\DailyWorkingTimeChartProvider; +use App\Repository\TimesheetRepository; use App\Widget\Type\DailyWorkingTimeChart; use App\Widget\WidgetInterface; use PHPUnit\Framework\TestCase; @@ -27,7 +24,7 @@ class DailyWorkingTimeChartTest extends TestCase { public function createSut(): DailyWorkingTimeChart { - $repository = $this->createMock(DailyWorkingTimeChartProvider::class); + $repository = $this->createMock(TimesheetRepository::class); $sut = new DailyWorkingTimeChart($repository); $sut->setUser(new User()); @@ -65,54 +62,4 @@ class DailyWorkingTimeChartTest extends TestCase self::assertStringStartsWith('DailyWorkingTimeChart_', $options['id']); self::assertEquals('xxx', $options['type']); } - - public function testGetData(): void - { - $activity = $this->createMock(Activity::class); - $activity->method('getId')->willReturn(42); - - $project = $this->createMock(Project::class); - $project->method('getId')->willReturn(4711); - - $repository = $this->getMockBuilder(DailyWorkingTimeChartProvider::class)->disableOriginalConstructor()->onlyMethods(['getDailyData'])->getMock(); - $repository->expects($this->once())->method('getDailyData')->willReturnCallback(function ($begin, $end, $user) use ($activity, $project) { - return [ - [ - 'year' => $begin->format('Y'), - 'month' => $begin->format('n'), - 'day' => $begin->format('j'), - 'rate' => 13.75, - 'duration' => 1234, - 'billable' => 1234, - 'details' => [ - [ - 'activity' => $activity, - 'project' => $project, - 'billable' => 1234, - ] - ] - ] - ]; - }); - - $sut = new DailyWorkingTimeChart($repository); - $sut->setUser(new User()); - $data = $sut->getData($sut->getOptions()); - self::assertCount(2, $data); - self::assertArrayHasKey('activities', $data); - self::assertArrayHasKey('data', $data); - - self::assertCount(1, $data['activities']); - self::assertArrayHasKey('4711_42', $data['activities']); - self::assertCount(2, $data['activities']['4711_42']); - self::assertArrayHasKey('activity', $data['activities']['4711_42']); - self::assertArrayHasKey('project', $data['activities']['4711_42']); - self::assertSame($activity, $data['activities']['4711_42']['activity']); - self::assertSame($project, $data['activities']['4711_42']['project']); - - self::assertCount(7, $data['data']); - foreach ($data['data'] as $statObj) { - self::assertInstanceOf(Day::class, $statObj); - } - } } diff --git a/tests/phpstan.neon b/tests/phpstan.neon index d08a4de2..7d155305 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -5,6 +5,7 @@ includes: - %rootDir%/../phpstan-doctrine/rules.neon - %rootDir%/../phpstan-phpunit/extension.neon - %rootDir%/../phpstan-phpunit/rules.neon + - %rootDir%/../phpstan-deprecation-rules/rules.neon parameters: level: 9 @@ -4132,16 +4133,6 @@ parameters: count: 1 path: Widget/Type/AmountYearTest.php - - - message: "#^Parameter \\#2 \\$array of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) expects array\\|ArrayAccess, mixed given\\.$#" - count: 1 - path: Widget/Type/DailyWorkingTimeChartTest.php - - - - message: "#^Parameter \\#2 \\$haystack of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertCount\\(\\) expects Countable\\|iterable, mixed given\\.$#" - count: 1 - path: Widget/Type/DailyWorkingTimeChartTest.php - - message: "#^Method App\\\\Tests\\\\Widget\\\\Type\\\\DurationYearTest\\:\\:getDefaultOptions\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 7a71b6b4..cb781884 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1544,10 +1544,18 @@ DeactivatedDeaktiviert + + Deactivate + Deaktivieren + Activated Aktiviert + + Activate + Aktivieren + about.title Über Kimai diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 183eb552..11af80a2 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -1544,10 +1544,18 @@ Deactivated Deactivated + + Deactivate + Deactivate + Activated Activated + + Activate + Activate + about.title About Kimai
{{ 'name'|trans }}