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 @@
{{ 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 %}
-
+
| {{ 'name'|trans }} |
@@ -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 @@
Deactivated
Deaktiviert
+
+ 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