Compare commits

..

9 Commits
dev ... 2.49.0

Author SHA1 Message Date
Kevin Papst
ff6918fcab Release 2.49 (#5820)
* bump packages
* add column summarization per customer (#5808)
* clarify database requirements
* added helper methods to fetch original expected time

Co-authored-by: GregorB54321 <34287148+GregorB54321@users.noreply.github.com>
2026-02-15 21:31:06 +01:00
Weblate (bot)
f376b5c8a1 Translated using Weblate (#5804)
Co-authored-by: AlaxLima <thanhkhoidangngoc@gmail.com>
Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Co-authored-by: Artem <artemkozhin80@gmail.com>
Co-authored-by: Eleni Diamantopoulou <elenidiama00@gmail.com>
Co-authored-by: Florent Berthelot <florentius.b@gmail.com>
Co-authored-by: Kehribar <103407696+dpentx@users.noreply.github.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Lasse Edsvik <lasse@lasseedsvik.se>
Co-authored-by: Levente Déri <derilevi@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Paul <snide-slum-partake@duck.com>
Co-authored-by: Posemartonis <weblate.drainage895@passmail.net>
Co-authored-by: Romhányi-Kakucska Viktor <viktor@romhanyi.dev>
Co-authored-by: irooniline <mart.styff@protonmail.com>
Co-authored-by: leonidovmob <leonidovmob@gmail.com>
2026-02-15 21:28:53 +01:00
Kevin Papst
b00fbd2516 Delete .opensourcefinder-verify 2026-02-14 18:56:38 +01:00
Kevin Papst
d97abc3f57 Add .opensourcefinder-verify file with claims (#5824) 2026-02-14 08:19:51 +01:00
Kevin Papst
3f184e42c8 missing translations 2026-01-31 10:28:30 +01:00
Kevin Papst
4a31411d69 Release 2.48 (#5789) 2026-01-30 16:44:26 +01:00
Weblate (bot)
3925eacf9f Translated using Weblate (#5748)
Co-authored-by: AlaxLima <thanhkhoidangngoc@gmail.com>
Co-authored-by: Amir <amearb@duck.com>
Co-authored-by: Fabio Gomes de lima <fabiogomesdelima598@gmail.com>
Co-authored-by: Heeheon Ryu <heeheon.ryu001@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Kolappan N <kolappan@users.noreply.hosted.weblate.org>
Co-authored-by: Lasse Edsvik <lasse@lasseedsvik.se>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Mauro F. T. <maurofroeltani@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pose marto <weblate.drainage895@passmail.net>
Co-authored-by: Serhii Horichenko <serhii@horichenko.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: avv-dev <vildan.abdullin@gmail.com>
Co-authored-by: m45ked <m45ked@users.noreply.hosted.weblate.org>
2026-01-30 13:14:21 +01:00
Kevin Papst
394f377890 Update header (#5799) 2026-01-28 20:10:33 +01:00
Milo Ivir
e8b0dc4969 Add translation links to README (#5798)
Co-authored-by: Kevin Papst <kevinpapst@users.noreply.github.com>
2026-01-28 19:58:12 +01:00
672 changed files with 11896 additions and 8297 deletions

View File

@@ -1,17 +1,23 @@
# editorconfig.org
; top-most EditorConfig file
root = true
; Unix-style newlines
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
charset = utf-8
[*.php]
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 4
[{compose.yaml,compose.*.yaml}]
indent_size = 2
[*.twig]
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false
[*.yaml]
indent_style = space
indent_size = 4
[*.yml]
indent_style = space
indent_size = 4

View File

@@ -1,18 +1,7 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
# You should NOT use this file in production, but instead move the environment variables to your webserver configuration.
# The .env file is only existing to simplify the initial setup!
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
#================================================================================
# Configure your database connection and set the correct server version.
@@ -27,34 +16,32 @@
# For MySQL that would be "serverVersion=5.7" as in:
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7
#
# For MariaDB it would be "serverVersion=10.11.15-MariaDB":
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.11.15-MariaDB
# For MariaDB it would be "serverVersion=10.5.8-MariaDB":
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
#
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.11.15-MariaDB
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
#================================================================================
# The full documentation can be found at https://www.kimai.org/documentation/emails.html
#
# Email will be sent with this address as sender:
MAILER_FROM=kimai@example.com
# Email connection (disabled by default) - see documentation for the format
MAILER_URL=null://null
#================================================================================
# do not change, unless you are developing for Kimai
APP_ENV=prod
#================================================================================
# should be changed to a unique character sequence, used for hashing cookies
APP_SECRET=change_this_to_something_unique
#================================================================================
# Running behind reverse proxies? Try these:
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
# TRUSTED_HOSTS=localhost|example.com
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###
###> symfony/mailer ###
# Documentation at https://www.kimai.org/documentation/emails.html
MAILER_FROM=kimai@example.com
# Email connection (disabled by default) - see documentation for the format
MAILER_URL=null://null
###< symfony/mailer ###
###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###< nelmio/cors-bundle ###
###> symfony/routing ###
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost
###< symfony/routing ###
#================================================================================
# unlikely, that you need to change this one
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$

2
.gitattributes vendored
View File

@@ -17,4 +17,4 @@ phpstan.neon export-ignore
phpstan.sh export-ignore
phpunit.xml.dist export-ignore
webpack.config.js export-ignore
yarn.lock export-ignore
yarn.lock export-ignore

View File

@@ -30,7 +30,7 @@ jobs:
- name: Determine version
run: |
input="${{ github.event.inputs.kimai_tag }}"
# Determine between manual trigger and release event
if [ -z "$input" ]; then
echo "Using release tag: ${{ github.event.release.tag_name }}"
@@ -44,7 +44,7 @@ jobs:
echo "Invalid version number: $version"
exit 1
fi
echo "kimai_version=$version" >> $GITHUB_ENV
- name: FPM image
@@ -73,6 +73,7 @@ jobs:
target: prod
platforms: linux/amd64,linux/arm64
tags: |
kimai/kimai2:stable
kimai/kimai2:apache
kimai/kimai2:apache-${{ env.kimai_version }}
push: true

View File

@@ -19,7 +19,7 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
matrix:
php: ['8.4', '8.5']
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
name: Integration (${{ matrix.php }})
steps:

8
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# some hosters require a htaccess to change the PHP version
/public/.htaccess
/.env-*
/.idea/
.DS_Store
var/templates/
@@ -23,7 +24,6 @@ var/templates/
/var/dev/*
/var/data/*
/var/cache/*
/var/share/*
/var/invoices*
/var/export*
/var/log/*
@@ -37,6 +37,7 @@ var/templates/
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
.env
/public/bundles/
/vendor/
###< symfony/framework-bundle ###
@@ -47,8 +48,9 @@ var/templates/
###< phpunit/phpunit ###
###> friendsofphp/php-cs-fixer ###
/.php-cs-fixer.php
/.php-cs-fixer.cache
.php_cs
.php_cs.cache
.php-cs-fixer.cache
###< friendsofphp/php-cs-fixer ###
###> symfony/phpunit-bridge ###

View File

@@ -6,11 +6,9 @@
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/actions/workflows/testing.yaml/badge.svg"></a>
<a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a>
<a href="https://packagist.org/packages/kimai/kimai"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
<a href="https://phpc.social/@kimai" rel="me"><img alt="Mastodon" src="https://img.shields.io/badge/toot-%40kimai-8c8dff"></a>
</p>
<h1 align="center">Kimai - time-tracker</h1>
<h1 align="center">Kimai<br>#1 Open-Source Time-Tracker</h1>
Kimai is a professional grade time-tracking application, free and open-source.
It handles use-cases of freelancers as well as companies with dozens or hundreds of users.
@@ -30,7 +28,7 @@ and so much more.
### Requirements
- PHP 8.1.3 minimum with support for 8.2, 8.3, 8.4, 8.5
- MariaDB or MySQL
- MariaDB / MySQL in their oldest maintained LTS release, currently MariaDB >= [10.6](https://endoflife.date/mariadb) or MySQL >= [8.4](https://endoflife.date/mysql)
- A webserver and subdomain (subdirectory is not supported)
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
@@ -71,6 +69,7 @@ The best way to start is to [open a new issue](https://github.com/kimai/kimai/is
In case you want to contribute, but you wouldn't know how, here are some suggestions:
- Spread the word: Please [write a testimonial for our Wall of love](https://love.kimai.org), vote for Kimai on any software platform, you can toot or tweet about it, share it on LinkedIn, Reddit and any other social media platform!
- [Translate Kimai into your language](https://hosted.weblate.org/engage/kimai/), or help to improve the existing translations, many languages look for a contributor
- Answer questions: You know the answer to another user's problem? Share your knowledge.
- Something can be done better? An essential feature is missing? Create a feature request.
- Report bugs makes Kimai better for everyone.
@@ -79,6 +78,12 @@ In case you want to contribute, but you wouldn't know how, here are some suggest
There is one simple rule in our "Code of conduct": Don't be an ass!
## Follow Kimai
- Mastodon: [@kimai](https://phpc.social/@kimai)
- Youtube: [@kimai_org](https://www.youtube.com/@kimai_org)
- LinkedIn: [@kimai-org](https://www.linkedin.com/company/kimai-org/)
### Credits
Kimai is based on modern technologies and frameworks such as [PHP](https://www.php.net/),

View File

@@ -1,50 +0,0 @@
# Upgrading Kimai - Version 2.x
_Make sure to create a backup before you start!_
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
**!! This release requires minimum PHP version to 8.1 !!**
### Breaking changes
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
- removed: configuring the `dashboard` is not supported any longer
- removed: custom translation files via `theme.branding.translation`
- removed: changing the plugin directory via `kimai.plugin_dir`
### Developer
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
- Role names are forced to be uppercase
- Removed unused `public/avatars/` directory
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
- `date_full` => `date_time`
- `duration_decimal` => `duration(true)`
- `currency` => `currency_name`
- `country` => `country_name`
- `language` => `language_name`
- Removed support for custom translation files (use [TranslationBundle](https://www.kimai.org/store/translation-bundle.html) instead or write your own plugin)
- Removed all 3rd party mailer packages, you need to install them manually (ONLY if you used a short syntax to configure the `MAILER_URL` in `.env`):
- `composer require symfony/amazon-mailer`
- `composer require symfony/google-mailer`
- `composer require symfony/mailchimp-mailer`
- `composer require symfony/mailgun-mailer`
- `composer require symfony/postmark-mailer`
- `composer require symfony/sendgrid-mailer`

View File

@@ -11,27 +11,16 @@ Perform EACH version specific task between your version and the new one, otherwi
**!! This release requires minimum PHP version 8.4 !!**
### Rename .env
Rename your file `.env` to `.env.local` or even better: move all variables to your webserver/container environment.
### Developer
Do not use method chaining: all fluent interface, especially in Entities, are no longer supported.
Do not use method chaining: all fluent interface, especially in Entities, are no longer supported.
- Require PHP 8.4
- Bump to Symfony 7.4
- Removed old API token `X-AUTH-USER` and `X-AUTH-TOKEN`
- Removed `TimesheetConstraint` - use a normal `Constraint` as base class and attach the `#[App\Validator\Attribute\TimesheetConstraint]` attribute
- Removed `ProjectConstraint` - use a `FormExtension` and attach your custom constraints
- Interface `MetaTableTypeInterface` has new methods: `getSection()`, `setSection()`
- Interface `ExportRendererInterface` has new methods: `getType()`, `isInternal()`
- Interface `ExportableItem` has new methods: `getTags()`, `getBreak()`
- Removed and renamed translations, most important `action.edit` => `edit`, `my.profile` => `user_profile`
- Removed `User::isExportDecimal()`
- Use duration format `HH:mm` in default PDF exports
- Replace Twig `AppVariable` with custom implementation
- You need to adjust your templates if you access anything else then `app.locale`, `app.user`. `app.current_route`.
- Most often used:
- Replace `app.request.locale` with `app.locale`
- Replace `app.request.attributes.get('_route')` with `app.current_route`
Removed translations:
- `action.edit`: use `edit` instead
- `my.profile`: use `user_profile` instead
- `stats.userAmountToday`: use `` instead
- `stats.userAmountWeek`: use `` instead
- `stats.userAmountMonth`: use `` instead
- `stats.userAmountYear`: use `` instead
- `stats.userAmountTotal`: use `` instead
- `update_multiple`

View File

@@ -1,3 +1,50 @@
# Upgrading Kimai
# Upgrading Kimai - Version 2.x
The most recent version is Kimai 3, please read the changelog at [UPGRADING-3.md](UPGRADING-3.md).
_Make sure to create a backup before you start!_
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
**!! This release requires minimum PHP version to 8.1 !!**
### Breaking changes
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
- removed: configuring the `dashboard` is not supported any longer
- removed: custom translation files via `theme.branding.translation`
- removed: changing the plugin directory via `kimai.plugin_dir`
### Developer
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
- Role names are forced to be uppercase
- Removed unused `public/avatars/` directory
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
- `date_full` => `date_time`
- `duration_decimal` => `duration(true)`
- `currency` => `currency_name`
- `country` => `country_name`
- `language` => `language_name`
- Removed support for custom translation files (use [TranslationBundle](https://www.kimai.org/store/translation-bundle.html) instead or write your own plugin)
- Removed all 3rd party mailer packages, you need to install them manually (ONLY if you used a short syntax to configure the `MAILER_URL` in `.env`):
- `composer require symfony/amazon-mailer`
- `composer require symfony/google-mailer`
- `composer require symfony/mailchimp-mailer`
- `composer require symfony/mailgun-mailer`
- `composer require symfony/postmark-mailer`
- `composer require symfony/sendgrid-mailer`

View File

@@ -43,7 +43,6 @@ import KimaiHotkeys from "./plugins/KimaiHotkeys";
import KimaiRemoteModal from "./plugins/KimaiRemoteModal";
import KimaiUser from "./plugins/KimaiUser";
import KimaiAutocompleteTags from "./forms/KimaiAutocompleteTags";
import KimaiMonthPicker from "./forms/KimaiMonthPicker";
export default class KimaiLoader {
@@ -77,7 +76,6 @@ export default class KimaiLoader {
kimai.registerPlugin(new KimaiTeamForm());
kimai.registerPlugin(new KimaiCopyDataForm());
kimai.registerPlugin(new KimaiDateNowForm());
kimai.registerPlugin(new KimaiMonthPicker());
kimai.registerPlugin(new KimaiForm());
kimai.registerPlugin(new KimaiHotkeys());

View File

@@ -1,44 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDatePicker: single date selects (currently unused)
*/
import KimaiFormPlugin from "./KimaiFormPlugin";
export default class KimaiMonthPicker extends KimaiFormPlugin {
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form) // eslint-disable-line no-unused-vars
{
return true;
}
/**
* @param {HTMLFormElement} form
*/
activateForm(form)
{
const input = document.createElement('input');
input.setAttribute('type','month');
const notADateValue = 'not-a-month';
input.setAttribute('value', notADateValue);
if (input.value === notADateValue) {
const polyfills = form.querySelectorAll('a.input-month-polyfill');
polyfills.forEach(i => i.classList.toggle('d-none'));
const inputs = form.querySelectorAll('input[type="month"]');
inputs.forEach(i => i.classList.toggle('d-none'));
}
}
}

View File

@@ -1,3 +1,4 @@
@import "variables";
@import "layout";
@import "error-page";
@import "print";

View File

@@ -0,0 +1,33 @@
:root {
--kimai-public-holiday: var(--tblr-lime);
--kimai-holiday: var(--tblr-green);
--kimai-sickness: var(--tblr-yellow);
--kimai-time-off: var(--tblr-blue);
--kimai-other: var(--tblr-purple);
--kimai-public-holiday-bg: var(--tblr-lime-lt);
--kimai-holiday-bg: var(--tblr-green-lt);
--kimai-sickness-bg: var(--tblr-yellow-lt);
--kimai-time-off-bg: var(--tblr-blue-lt);
--kimai-other-bg: var(--tblr-purple-lt);
--kimai-unexpected-bg: var(--tblr-pink-lt);
--kimai-missing-bg: var(--tblr-pink-lt);
--kimai-weekend-bg: var(--tblr-bg-surface-tertiary);
}
.public-holiday { color: var(--kimai-public-holiday); }
.holiday { color: var(--kimai-holiday); }
.sickness { color: var(--kimai-sickness); }
.time-off { color: var(--kimai-time-off); }
.other { color: var(--kimai-other); }
.bg-public-holiday{ background-color: var(--kimai-public-holiday-bg); --tblr-table-bg: var(--kimai-public-holiday-bg); i.fas{ color: var(--kimai-public-holiday); } };
.bg-holiday { background-color: var(--kimai-holiday-bg); --tblr-table-bg: var(--kimai-holiday-bg); i.fas{ color: var(--kimai-holiday); } };
.bg-sickness { background-color: var(--kimai-sickness-bg); --tblr-table-bg: var(--kimai-sickness-bg); i.fas{ color: var(--kimai-sickness); } };
.bg-time-off { background-color: var(--kimai-time-off-bg); --tblr-table-bg: var(--kimai-time-off-bg); i.fas{ color: var(--kimai-time-off); } };
.bg-other { background-color: var(--kimai-other-bg); --tblr-table-bg: var(--kimai-other-bg); i.fas{ color: var(--kimai-other); } };
.bg-unexpected { background-color: var(--kimai-unexpected-bg); --tblr-table-bg: var(--kimai-unexpected-bg); };
.bg-missing { background-color: var(--kimai-missing-bg); --tblr-table-bg: var(--kimai-missing-bg); };
.bg-weekend { background-color: var(--kimai-weekend-bg); --tblr-table-bg: var(--kimai-weekend-bg); };

View File

@@ -6,8 +6,8 @@ use App\ConsoleApplication;
set_time_limit(0);
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

View File

@@ -14,7 +14,7 @@
}
],
"require": {
"php": "8.4.*||8.5.*",
"php": "8.1.*||8.2.*||8.3.*||8.4.*||8.5.*",
"ext-gd": "*",
"ext-intl": "*",
"ext-json": "*",
@@ -29,10 +29,11 @@
"composer/semver": "^3.3",
"doctrine/doctrine-bundle": "^2.7",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.0",
"doctrine/orm": "^2.8",
"easybill/zugferd-php": "^2.1",
"endroid/qr-code": "^6.0",
"friendsofsymfony/rest-bundle": "3.9.0-beta1",
"endroid/qr-code": "^4.8",
"erusev/parsedown": "^1.6",
"friendsofsymfony/rest-bundle": "^3.0",
"gedmo/doctrine-extensions": "^3.6",
"horstoeko/zugferd": "^1.0",
"horstoeko/zugferdublbridge": "^1.0",
@@ -44,39 +45,37 @@
"nelmio/cors-bundle": "^2.0",
"onelogin/php-saml": "^4.0",
"openspout/openspout": "^4.0",
"pagerfanta/pagerfanta": "^4.0",
"parsedown/parsedown": "^1.6",
"pagerfanta/pagerfanta": "^3.0",
"phpoffice/phpspreadsheet": "^2.0",
"phpoffice/phpword": "^1.0",
"psr/container": "^2.0",
"psr/log": "^3.0",
"scheb/2fa-backup-code": "^7.11",
"scheb/2fa-bundle": "^7.11",
"scheb/2fa-totp": "^7.11",
"symfony/asset": "^7.0",
"symfony/console": "^7.0",
"symfony/dependency-injection": "^7.0",
"symfony/dotenv": "^7.0",
"symfony/expression-language": "^7.0",
"scheb/2fa-backup-code": "^6.2",
"scheb/2fa-bundle": "^6.2",
"scheb/2fa-totp": "^6.2",
"symfony/asset": "^6.0",
"symfony/console": "^6.0",
"symfony/dotenv": "^6.0",
"symfony/expression-language": "^6.0",
"symfony/flex": "^2",
"symfony/form": "^7.0",
"symfony/framework-bundle": "^7.0",
"symfony/http-client": "^7.0",
"symfony/intl": "^7.0",
"symfony/mailer": "^7.0",
"symfony/mime": "^7.0",
"symfony/form": "^6.0",
"symfony/framework-bundle": "^6.0",
"symfony/http-client": "^6.0",
"symfony/intl": "^6.0",
"symfony/mailer": "^6.0",
"symfony/mime": "^6.0",
"symfony/monolog-bundle": "^3.4",
"symfony/process": "^7.0",
"symfony/rate-limiter": "^7.0",
"symfony/runtime": "^7.0",
"symfony/security-bundle": "^7.0",
"symfony/security-csrf": "^7.0",
"symfony/serializer": "^7.0",
"symfony/translation": "^7.0",
"symfony/twig-bundle": "^7.0",
"symfony/validator": "^7.0",
"symfony/process": "^6.0",
"symfony/rate-limiter": "^6.0",
"symfony/runtime": "^6.0",
"symfony/security-bundle": "^6.0",
"symfony/security-csrf": "^6.0",
"symfony/serializer": "^6.0",
"symfony/translation": "^6.0",
"symfony/twig-bundle": "^6.0",
"symfony/validator": "^6.0",
"symfony/webpack-encore-bundle": "^2.0",
"symfony/yaml": "^7.0",
"symfony/yaml": "^6.0",
"twig/cssinliner-extra": "^3.0",
"twig/extra-bundle": "^3.0",
"twig/inky-extra": "^3.0",
@@ -96,14 +95,14 @@
"phpstan/phpstan-strict-rules": "^2.0",
"phpstan/phpstan-symfony": "^2.0",
"phpunit/phpunit": "^10.0",
"symfony/browser-kit": "^7.0",
"symfony/css-selector": "^7.0",
"symfony/debug-bundle": "^7.0",
"symfony/dom-crawler": "^7.0",
"symfony/phpunit-bridge": "^7.0",
"symfony/stopwatch": "^7.0",
"symfony/var-dumper": "^7.0",
"symfony/web-profiler-bundle": "^7.0"
"symfony/browser-kit": "^6.0",
"symfony/css-selector": "^6.0",
"symfony/debug-bundle": "^6.0",
"symfony/dom-crawler": "^6.0",
"symfony/phpunit-bridge": "^6.0",
"symfony/stopwatch": "^6.0",
"symfony/var-dumper": "^6.0",
"symfony/web-profiler-bundle": "^6.0"
},
"repositories": [
{
@@ -118,7 +117,7 @@
},
"optimize-autoloader": true,
"platform": {
"php": "8.4.4"
"php": "8.1.3"
},
"preferred-install": {
"*": "dist"
@@ -145,9 +144,6 @@
"symfony/polyfill-intl-idn": "*",
"symfony/polyfill-intl-normalizer": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php84": "*",
"symfony/polyfill-php83": "*",
"symfony/polyfill-php82": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php74": "*",
@@ -213,7 +209,7 @@
"symfony": {
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
"allow-contrib": true,
"require": "7.4.*"
"require": "6.4.*"
}
}
}

3130
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ return [
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
FOS\RestBundle\FOSRestBundle::class => ['all' => true],

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
packages:
avatars:
base_path: 'avatars'

View File

@@ -0,0 +1,17 @@
framework:
cache:
#app: cache.adapter.redis
#default_redis_provider: redis://127.0.0.1:6379
#app: cache.adapter.memcached
#default_memcached_provider: 'memcached://localhost'
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@@ -1,5 +0,0 @@
when@test:
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true

View File

@@ -1,5 +0,0 @@
when@dev:
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

View File

@@ -10,31 +10,29 @@ doctrine:
default_connection: default
connections:
default:
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
# existing migrations will fail if the schema filter is activated
#schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~
url: '%env(resolve:DATABASE_URL)%'
url: '%env(DATABASE_URL)%'
driver: 'pdo_mysql'
charset: utf8mb4
default_table_options:
charset: utf8mb4
collation: utf8mb4_unicode_ci
schema_manager_factory: doctrine.dbal.default_schema_manager_factory
types:
datetime: App\Doctrine\UTCDateTimeType
datetime_immutable: App\Doctrine\UTCDateTimeImmutableType
orm:
controller_resolver:
auto_mapping: false
auto_generate_proxy_classes: true
# FIXME this is causing a deprecation and needs to be changed
# auto_mapping: false
auto_mapping: true
auto_generate_proxy_classes: '%kernel.debug%'
default_entity_manager: default
enable_native_lazy_objects: true
enable_lazy_ghost_objects: true
entity_managers:
default:
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
validate_xml_mapping: true
report_fields_where_declared: true
connection: default
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
@@ -58,23 +56,17 @@ when@test:
connections:
default:
logging: false
use_savepoints: true
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
metadata_cache_driver:
type: pool
pool: doctrine.system_cache_pool
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@@ -1,11 +1,8 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false
transactional: false
storage:
table_storage:
table_name: 'migration_versions'
custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt'
migrations_paths:
'DoctrineMigrations': '%kernel.project_dir%/migrations'
custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt'

View File

@@ -1,4 +1,3 @@
# Read the documentation: https://fosrestbundle.readthedocs.io/en/3.x/
fos_rest:
param_fetcher_listener:
enabled: true

View File

@@ -37,9 +37,6 @@ framework:
php_errors:
log: true
property_info:
with_constructor_extractor: true
mailer:
dsn: '%env(MAILER_URL)%'

View File

@@ -3,6 +3,10 @@ jms_serializer:
datetime:
default_format: 'Y-m-d\TH:i:sO' # DATE_ISO8601
visitors:
json_serialization:
options:
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION
xml_serialization:
format_output: '%kernel.debug%'
metadata:
@@ -14,20 +18,3 @@ jms_serializer:
excluded: []
property_naming:
id: 'jms_serializer.identical_property_naming_strategy'
when@prod:
jms_serializer:
visitors:
json_serialization:
options:
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION
when@dev:
jms_serializer:
visitors:
json_serialization:
options:
- JSON_PRETTY_PRINT
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION

View File

@@ -1,50 +1,5 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: fingers_crossed
action_level: notice
handler: file_log
excluded_http_codes:
- {400: ['^/api/']}
- {401: ['^/api/']}
- 403
- 404
- 405
channels: ["!event", "!deprecation"]
file_log:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
formatter: monolog.formatter.kimai
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!deprecation"]
deprecation:
type: stream
channels: ["deprecation"]
path: "%kernel.logs_dir%/deprecations.log"
formatter: monolog.formatter.deprecation
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
formatter: monolog.formatter.kimai
channels: ["deprecation"]
when@prod:
monolog:
@@ -69,3 +24,42 @@ when@prod:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!deprecation"]
when@dev:
monolog:
handlers:
main:
type: fingers_crossed
action_level: notice
handler: file_log
excluded_http_codes:
- {400: ['^/api/']}
- {401: ['^/api/']}
- 403
- 404
- {405: ['/homepage$', '/login_check$', '/export/data$']}
channels: ["!event", "!deprecation"]
file_log:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
formatter: monolog.formatter.kimai
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!deprecation"]
deprecation:
type: stream
channels: ["deprecation"]
path: "%kernel.logs_dir%/deprecations.log"
formatter: monolog.formatter.deprecation
when@test:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: info
channels: ["!event"]
formatter: monolog.formatter.kimai

View File

@@ -17,7 +17,7 @@ nelmio_cors:
'^/api/':
# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_origin: ['*']
allow_headers: ['Content-Type', 'Authorization']
allow_headers: ['Content-Type', 'Authorization', 'X-AUTH-USER', 'X-AUTH-TOKEN']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
# expose_headers: ['Link']
max_age: 3600

View File

@@ -1,6 +1,3 @@
parameters:
env(default_uri): 'http://localhost'
framework:
router:
utf8: true
@@ -8,7 +5,7 @@ framework:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
default_uri: '%env(DEFAULT_URI)%'
#default_uri: http://localhost
when@test:
framework:

View File

@@ -1,8 +1,7 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
App\Entity\User: auto
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
chain_provider:
chain:
@@ -12,11 +11,12 @@ security:
class: App\Entity\User
kimai_ldap:
id: App\Ldap\LdapUserProvider
firewalls:
dev:
# Ensure dev tools and static assets are always allowed
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
access_token:
token_handler: App\API\Authentication\AccessTokenHandler
@@ -27,52 +27,66 @@ security:
stateless: true
remember_me: false
provider: chain_provider
custom_authenticators:
- App\API\Authentication\TokenAuthenticator
secured_area:
kimai_ldap: ~
pattern: ^/
user_checker: App\Security\UserChecker
stateless: false
entry_point: form_login
custom_authenticators:
- App\Saml\SamlAuthenticator
remember_me:
name: KIMAI_REMEMBER
secret: '%kernel.secret%'
lifetime: 604800
path: /
always_remember_me: true
# activate all configured user provider
provider: chain_provider
form_login:
check_path: security_check
login_path: login
enable_csrf: true
two_factor:
auth_form_path: 2fa_login
check_path: 2fa_login_check
remember_me_sets_trusted: true
logout:
path: logout
target: homepage
enable_csrf: false
login_throttling:
max_attempts: 5
interval: '5 minutes'
login_link:
check_route: link_login_check
signature_properties: ['id']
lifetime: 900
max_uses: 3
access_decision_manager:
# only grants access if there is no voter denying access
strategy: unanimous
allow_if_all_abstain: false
role_hierarchy:
ROLE_USER: ~
ROLE_TEAMLEAD: ROLE_USER
ROLE_ADMIN: ROLE_TEAMLEAD
ROLE_SUPER_ADMIN: ROLE_ADMIN
# Note: Only the *first* matching rule is applied
access_control:
- {path: '^/auth/2fa', role: IS_AUTHENTICATED_2FA_IN_PROGRESS}
- {path: '^/auth', roles: PUBLIC_ACCESS}
@@ -85,12 +99,13 @@ security:
- {path: '^/api', roles: IS_AUTHENTICATED}
when@test:
# this configuration simplifies testing URLs protected by the security mechanism
# See https://symfony.com/doc/current/cookbook/testing/http_authentication.html
security:
password_hashers:
# Password hashers are resource-intensive by design to ensure security.
# In tests, it's safe to reduce their cost to improve performance.
App\Entity\User:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
# see https://github.com/symfony/recipes/pull/1026
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

View File

@@ -75,6 +75,7 @@ tabler:
fax: fas fa-fax
filter: fas fa-filter
help: far fa-question-circle
holiday: fas fa-umbrella-beach
home: fas fa-home
info: fas fa-info-circle
import: fas fa-file-import
@@ -95,6 +96,7 @@ tabler:
ods: fas fa-table
off: fas fa-toggle-off
on: fas fa-toggle-on
other: fas fa-file-alt
password: fas fa-key
pause: fas fa-pause
pause-small: far fa-pause-circle
@@ -108,6 +110,7 @@ tabler:
profile: fas fa-user-edit
profile-stats: far fa-chart-bar
project: fas fa-briefcase
public-holiday: fas fa-calendar-day
repeat: fas fa-repeat
reporting: far fa-chart-bar
report: far fa-chart-bar
@@ -119,6 +122,7 @@ tabler:
save: far fa-save
search: fas fa-search
settings: fas fa-cog
sickness: fas fa-prescription-bottle-medical
shop: fas fa-shopping-cart
spinner: fas fa-spinner
start: fas fa-play
@@ -130,6 +134,7 @@ tabler:
team: fas fa-users
timesheet: fas fa-clock
timesheet-team: fas fa-user-clock
time-off: fas fa-couch
trash: far fa-trash-alt
unlocked: fas fa-unlock-alt
upload: fas fa-upload

View File

@@ -1,13 +1,9 @@
twig:
file_name_pattern: '*.twig'
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
form_themes:
- 'form/vertical.html.twig'
exception_controller: null
paths:
'%kernel.project_dir%/templates/bundles/TablerBundle': theme
'%kernel.project_dir%/vendor/kevinpapst/tabler-bundle/templates': theme
globals:
app: '@App\Twig\AppVariable'
when@test:
twig:
strict_variables: true

View File

@@ -1,13 +1,15 @@
when@dev:
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler:
collect_serializer_data: true
profiler: { only_exceptions: false }
when@test:
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler:
collect: false
collect_serializer_data: true
profiler: { collect: false }

View File

@@ -1,45 +1,17 @@
webpack_encore:
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
# The path where Encore is building the assets.
# This should match Encore.setOutputPath() in webpack.config.js.
output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
# Uncomment (also under link_attributes) if using Turbo Drive
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
# 'data-turbo-track': reload
# link_attributes:
# Uncomment if using Turbo Drive
# 'data-turbo-track': reload
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# if using Encore.enableIntegrityHashes() specify the crossorigin attribute value (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Available in version 1.2
cache: '%kernel.debug%'
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# If you have multiple builds:
# builds:
# frontend: '%kernel.project_dir%/public/frontend/build'
# pass the build name as the 3rd argument to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
when@prod:
when@dev:
webpack_encore:
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Available in version 1.2
cache: true
#when@test:
# webpack_encore:
# strict_mode: false
cache: false

View File

@@ -1,14 +1,4 @@
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
# This file is the entry point to configure the routes of your app.
# Methods with the #[Route] attribute are automatically imported.
# See also https://symfony.com/doc/current/routing.html
# To list all registered routes, run the following command:
# bin/console debug:router
controllers:
# resource: routing.controllers
resource: ../src/Controller/
type: attribute
prefix: /{_locale}
@@ -18,8 +8,7 @@ controllers:
api.swagger_ui:
path: /api/doc
methods: GET
defaults:
_controller: nelmio_api_doc.controller.stoplight
defaults: { _controller: nelmio_api_doc.controller.stoplight }
api:
resource: ../src/API/

View File

@@ -0,0 +1,5 @@
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /{_locale}/_error
defaults:
_locale: '%locale%'

View File

@@ -0,0 +1,7 @@
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler

View File

@@ -1,6 +0,0 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /{_locale}/_error
defaults:
_locale: '%locale%'

View File

@@ -1,3 +0,0 @@
_security_logout:
resource: security.route_loader.logout
type: service

View File

@@ -1,8 +0,0 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler

View File

@@ -1,11 +1,3 @@
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
locale: en
# can be regenerated with: bin/console kimai:reset:locales
@@ -14,8 +6,8 @@ parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
autowire: true
autoconfigure: true
public: false
bind:
$projectDirectory: '%kernel.project_dir%'
@@ -24,11 +16,12 @@ services:
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
resource: '../src/*'
exclude:
- '../src/API/Model/'
- '../src/DependencyInjection/'
- '../src/Doctrine/Extensions/'
- '../src/Entity/'
- '../src/Event/'
- '../src/Export/Package/'
- '../src/Export/Base/'
@@ -42,9 +35,6 @@ services:
- '../src/Kernel.php'
- '../src/Constants.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
@@ -237,13 +227,3 @@ services:
App\API\ViewHandler:
arguments: ['@fos_rest.view_handler.default']
App\Validator\Constraints\TimesheetAllValidator:
arguments: ['%kimai.validator_timesheet%']
App\Validator\Constraints\QuickEntryTimesheetValidator:
arguments: ['%kimai.validator_timesheet%']
App\Twig\AppVariable:
tags:
- {name: twig.global, alias: app}

25
config/services_test.yaml Normal file
View File

@@ -0,0 +1,25 @@
parameters:
locale: en
kimai_locales: ['ar', 'cs', 'de', 'de_CH', 'da', 'en', 'eo', 'es', 'eu', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pl', 'pt_BR', 'ro', 'ru', 'sk', 'sv', 'tr', 'vi', 'zh_CN']
services:
_defaults:
public: true
App\Configuration\SystemConfiguration:
arguments: ['@App\Configuration\ConfigurationService', "%kimai.config%"]
# required for the importer command test
App\Repository\UserRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\User']
# required for the importer command test
App\Importer\ImporterService:
public: true
arguments: ['@App\Customer\CustomerService', '@App\Project\ProjectService']
App\User\UserService:
public: true
arguments: ['@App\Repository\UserRepository', '@event_dispatcher', '@validator', '@App\Configuration\SystemConfiguration', '@security.user_password_hasher']

View File

View File

@@ -28,13 +28,13 @@ class Version20190510205245 extends AbstractMigration
$timesheetTags->addColumn('tag_id', 'integer', ['length' => 11, 'notnull' => true]);
$timesheetTags->addIndex(['timesheet_id'], 'IDX_E3284EFEABDD46BE');
$timesheetTags->addIndex(['tag_id'], 'IDX_E3284EFEBAD26311');
$this->addPrimaryKeyConstraint($timesheetTags, ['timesheet_id', 'tag_id']);
$timesheetTags->setPrimaryKey(['timesheet_id', 'tag_id']);
$tags = $schema->createTable('kimai2_tags');
$tags->addColumn('id', 'integer', ['length' => 11, 'autoincrement' => true, 'notnull' => true]);
$tags->addColumn('name', 'string', ['length' => 100, 'notnull' => true]);
$tags->addUniqueIndex(['name'], 'UNIQ_27CAF54C5E237E06');
$this->addPrimaryKeyConstraint($tags, ['id']);
$tags->setPrimaryKey(['id']);
}
public function down(Schema $schema): void

View File

@@ -34,7 +34,7 @@ final class Version20190617100845 extends AbstractMigration
$timesheetMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$timesheetMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
$timesheetMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
$this->addPrimaryKeyConstraint($timesheetMeta, ['id']);
$timesheetMeta->setPrimaryKey(['id']);
$timesheetMeta->addIndex(['timesheet_id'], 'IDX_CB606CBAABDD46BE');
$timesheetMeta->addUniqueIndex(['timesheet_id', 'name'], 'UNIQ_CB606CBAABDD46BE5E237E06');
$timesheetMeta->addForeignKeyConstraint('kimai2_timesheet', ['timesheet_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_CB606CBAABDD46BE');
@@ -45,7 +45,7 @@ final class Version20190617100845 extends AbstractMigration
$customerMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$customerMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
$customerMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
$this->addPrimaryKeyConstraint($customerMeta, ['id']);
$customerMeta->setPrimaryKey(['id']);
$customerMeta->addIndex(['customer_id'], 'IDX_A48A760F9395C3F3');
$customerMeta->addUniqueIndex(['customer_id', 'name'], 'UNIQ_A48A760F9395C3F35E237E06');
$customerMeta->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A48A760F9395C3F3');
@@ -56,7 +56,7 @@ final class Version20190617100845 extends AbstractMigration
$projectMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$projectMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
$projectMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
$this->addPrimaryKeyConstraint($projectMeta, ['id']);
$projectMeta->setPrimaryKey(['id']);
$projectMeta->addIndex(['project_id'], 'IDX_50536EF2166D1F9C');
$projectMeta->addUniqueIndex(['project_id', 'name'], 'UNIQ_50536EF2166D1F9C5E237E06');
$projectMeta->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50536EF2166D1F9C');
@@ -67,7 +67,7 @@ final class Version20190617100845 extends AbstractMigration
$activityMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$activityMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
$activityMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
$this->addPrimaryKeyConstraint($activityMeta, ['id']);
$activityMeta->setPrimaryKey(['id']);
$activityMeta->addIndex(['activity_id'], 'IDX_A7C0A43D81C06096');
$activityMeta->addUniqueIndex(['activity_id', 'name'], 'UNIQ_A7C0A43D81C060965E237E06');
$activityMeta->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A7C0A43D81C06096');

View File

@@ -32,7 +32,7 @@ final class Version20190730123324 extends AbstractMigration
$teams->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
$teams->addColumn('name', 'string', ['notnull' => true, 'length' => 100]);
$teams->addColumn('teamlead_id', 'integer', ['length' => 11, 'notnull' => true]);
$this->addPrimaryKeyConstraint($teams, ['id']);
$teams->setPrimaryKey(['id']);
$teams->addUniqueIndex(['name'], 'UNIQ_3BEDDC7F5E237E06');
$teams->addForeignKeyConstraint('kimai2_users', ['teamlead_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_3BEDDC7F8F7DE5D7');
@@ -41,21 +41,21 @@ final class Version20190730123324 extends AbstractMigration
$userTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
$userTeams->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8A76ED395');
$userTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8296CD8AE');
$this->addPrimaryKeyConstraint($userTeams, ['user_id', 'team_id']);
$userTeams->setPrimaryKey(['user_id', 'team_id']);
$customerTeams = $schema->createTable('kimai2_customers_teams');
$customerTeams->addColumn('customer_id', 'integer', ['length' => 11, 'notnull' => true]);
$customerTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
$customerTeams->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD83889395C3F3');
$customerTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD8388296CD8AE');
$this->addPrimaryKeyConstraint($customerTeams, ['customer_id', 'team_id']);
$customerTeams->setPrimaryKey(['customer_id', 'team_id']);
$projectTeams = $schema->createTable('kimai2_projects_teams');
$projectTeams->addColumn('project_id', 'integer', ['length' => 11, 'notnull' => true]);
$projectTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
$projectTeams->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431166D1F9C');
$projectTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431296CD8AE');
$this->addPrimaryKeyConstraint($projectTeams, ['project_id', 'team_id']);
$projectTeams->setPrimaryKey(['project_id', 'team_id']);
}
public function down(Schema $schema): void

View File

@@ -31,7 +31,7 @@ final class Version20191108151534 extends AbstractMigration
$roles = $schema->createTable('kimai2_roles');
$roles->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
$roles->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$this->addPrimaryKeyConstraint($roles, ['id']);
$roles->setPrimaryKey(['id']);
$roles->addUniqueIndex(['name'], 'roles_name');
$rolePermissions = $schema->createTable('kimai2_roles_permissions');
@@ -39,7 +39,7 @@ final class Version20191108151534 extends AbstractMigration
$rolePermissions->addColumn('role_id', 'integer', ['length' => 11, 'notnull' => true]);
$rolePermissions->addColumn('permission', 'string', ['notnull' => true, 'length' => 50]);
$rolePermissions->addColumn('allowed', 'boolean', ['notnull' => true, 'default' => false]);
$this->addPrimaryKeyConstraint($rolePermissions, ['id']);
$rolePermissions->setPrimaryKey(['id']);
$rolePermissions->addUniqueIndex(['role_id', 'permission'], 'role_permission');
$rolePermissions->addForeignKeyConstraint('kimai2_roles', ['role_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_D263A3B8D60322AC');
}

View File

@@ -35,7 +35,7 @@ final class Version20200109102138 extends AbstractMigration
$customerComment->addColumn('created_by_id', 'integer', ['notnull' => true]);
$customerComment->addColumn('created_at', 'datetime', ['notnull' => true]);
$customerComment->addColumn('pinned', 'boolean', ['notnull' => true, 'default' => false]);
$this->addPrimaryKeyConstraint($customerComment, ['id']);
$customerComment->setPrimaryKey(['id']);
$customerComment->addIndex(['customer_id'], 'IDX_A5B142D99395C3F3');
$customerComment->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A5B142D99395C3F3');
$customerComment->addForeignKeyConstraint('kimai2_users', ['created_by_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A5B142D9B03A8386');
@@ -47,7 +47,7 @@ final class Version20200109102138 extends AbstractMigration
$projectComment->addColumn('created_by_id', 'integer', ['notnull' => true]);
$projectComment->addColumn('created_at', 'datetime', ['notnull' => true]);
$projectComment->addColumn('pinned', 'boolean', ['notnull' => true, 'default' => false]);
$this->addPrimaryKeyConstraint($projectComment, ['id']);
$projectComment->setPrimaryKey(['id']);
$projectComment->addIndex(['project_id'], 'IDX_29A23638166D1F9C');
$projectComment->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_29A23638166D1F9C');
$projectComment->addForeignKeyConstraint('kimai2_users', ['created_by_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_29A23638B03A8386');

View File

@@ -37,7 +37,7 @@ final class Version20200205115243 extends AbstractMigration
$customerRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AECA76ED395');
$customerRates->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AEC9395C3F3');
$customerRates->addUniqueIndex(['user_id', 'customer_id'], 'UNIQ_82AB0AECA76ED3959395C3F3');
$this->addPrimaryKeyConstraint($customerRates, ['id']);
$customerRates->setPrimaryKey(['id']);
$projectRates = $schema->createTable('kimai2_projects_rates');
$projectRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
@@ -48,7 +48,7 @@ final class Version20200205115243 extends AbstractMigration
$projectRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55A76ED395');
$projectRates->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55166D1F9C');
$projectRates->addUniqueIndex(['user_id', 'project_id'], 'UNIQ_41535D55A76ED395166D1F9C');
$this->addPrimaryKeyConstraint($projectRates, ['id']);
$projectRates->setPrimaryKey(['id']);
$activityRates = $schema->createTable('kimai2_activities_rates');
$activityRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
@@ -59,7 +59,7 @@ final class Version20200205115243 extends AbstractMigration
$activityRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BEA76ED395');
$activityRates->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BE81C06096');
$activityRates->addUniqueIndex(['user_id', 'activity_id'], 'UNIQ_4A7F11BEA76ED39581C06096');
$this->addPrimaryKeyConstraint($activityRates, ['id']);
$activityRates->setPrimaryKey(['id']);
}
public function down(Schema $schema): void

View File

@@ -45,7 +45,7 @@ final class Version20200308171950 extends AbstractMigration
$invoices->addUniqueIndex(['invoice_filename'], 'UNIQ_76C38E372323B33D');
$invoices->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_76C38E37A76ED395');
$invoices->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_76C38E379395C3F3');
$this->addPrimaryKeyConstraint($invoices, ['id']);
$invoices->setPrimaryKey(['id']);
}
public function down(Schema $schema): void

View File

@@ -31,7 +31,7 @@ final class Version20200524142042 extends AbstractMigration
$sessions->addColumn('data', 'blob', ['length' => 65535, 'notnull' => true]);
$sessions->addColumn('time', 'integer', ['unsigned' => true, 'notnull' => true]);
$sessions->addColumn('lifetime', 'integer', ['unsigned' => true, 'notnull' => true]);
$this->addPrimaryKeyConstraint($sessions, ['id']);
$sessions->setPrimaryKey(['id']);
}
public function down(Schema $schema): void

View File

@@ -33,7 +33,7 @@ final class Version20200725213424 extends AbstractMigration
$activityTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
$activityTeams->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_986998DA81C06096');
$activityTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_986998DA296CD8AE');
$this->addPrimaryKeyConstraint($activityTeams, ['activity_id', 'team_id']);
$activityTeams->setPrimaryKey(['activity_id', 'team_id']);
}
public function down(Schema $schema): void

View File

@@ -36,7 +36,7 @@ final class Version20210316224358 extends AbstractMigration
$bookmarks->addColumn('content', 'text', ['notnull' => true]);
$bookmarks->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4016EF25A76ED395');
$bookmarks->addUniqueIndex(['user_id', 'name'], 'UNIQ_4016EF25A76ED3955E237E06');
$this->addPrimaryKeyConstraint($bookmarks, ['id']);
$bookmarks->setPrimaryKey(['id']);
}
public function down(Schema $schema): void

View File

@@ -30,7 +30,7 @@ final class Version20210802174318 extends AbstractMigration
$teamMember->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
$teamMember->addColumn('teamlead', 'boolean', ['notnull' => true, 'default' => false]);
$teamMember->dropPrimaryKey();
$this->addPrimaryKeyConstraint($teamMember, ['id']);
$teamMember->setPrimaryKey(['id']);
$teamMember->addUniqueIndex(['user_id', 'team_id'], 'UNIQ_B5E92CF8A76ED395296CD8AE');
}
@@ -41,6 +41,6 @@ final class Version20210802174318 extends AbstractMigration
$teamMember->dropPrimaryKey();
$teamMember->dropColumn('teamlead');
$teamMember->dropColumn('id');
$this->addPrimaryKeyConstraint($teamMember, ['user_id', 'team_id']);
$teamMember->setPrimaryKey(['user_id', 'team_id']);
}
}

View File

@@ -29,7 +29,7 @@ final class Version20220101204501 extends AbstractMigration
$table->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$table->addColumn('value', 'text', ['notnull' => false, 'length' => 65535]);
$table->addColumn('visible', 'boolean', ['notnull' => true, 'default' => false]);
$this->addPrimaryKeyConstraint($table, ['id']);
$table->setPrimaryKey(['id']);
$table->addIndex(['invoice_id'], 'IDX_7EDC37D92989F1FD');
$table->addUniqueIndex(['invoice_id', 'name'], 'UNIQ_7EDC37D92989F1FD5E237E06');
$table->addForeignKeyConstraint('kimai2_invoices', ['invoice_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_7EDC37D92989F1FD');

View File

@@ -35,7 +35,7 @@ final class Version20230327143628 extends AbstractMigration
$workingTimes->addColumn('expected', 'integer', ['notnull' => true]);
$workingTimes->addColumn('actual', 'integer', ['notnull' => true]);
$workingTimes->addColumn('approved_at', 'datetime', ['notnull' => false, 'default' => null]);
$this->addPrimaryKeyConstraint($workingTimes, ['id']);
$workingTimes->setPrimaryKey(['id']);
$workingTimes->addIndex(['user_id'], 'IDX_F95E4933A76ED395');
$workingTimes->addIndex(['approved_by'], 'IDX_F95E49334EA3CB3D');
$workingTimes->addUniqueIndex(['user_id', 'date'], 'UNIQ_F95E4933A76ED395AA9E377A');

View File

@@ -33,7 +33,7 @@ final class Version20240214061246 extends AbstractMigration
$accessTokens->addColumn('last_usage', 'datetime_immutable', ['notnull' => false, 'default' => null]);
$accessTokens->addColumn('expires_at', 'datetime_immutable', ['notnull' => false, 'default' => null]);
$this->addPrimaryKeyConstraint($accessTokens, ['id']);
$accessTokens->setPrimaryKey(['id']);
$accessTokens->addIndex(['user_id'], 'IDX_6FB0DB1EA76ED395');
$accessTokens->addUniqueIndex(['token'], 'UNIQ_6FB0DB1E5F37A13B');

View File

@@ -33,7 +33,7 @@ final class Version20250608143244 extends AbstractMigration
$table->addColumn('columns', 'json', ['notnull' => true]);
$table->addColumn('options', 'json', ['notnull' => true]);
$this->addPrimaryKeyConstraint($table, ['id']);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['title'], 'UNIQ_2F0CA26F2B36786B');
}

View File

@@ -1,109 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
/**
* @version 3.0
*/
final class Version20260101010000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Kimai 3.0 update';
}
public function up(Schema $schema): void
{
$connection = $this->connection;
$rows = $connection->fetchAllAssociative('SELECT id, roles FROM kimai2_users');
foreach ($rows as $row) {
$id = $row['id'];
$value = $row['roles'];
$data = [];
if (\is_string($value) && str_starts_with($value, 'a:')) {
$data = unserialize($value);
}
if (!\is_array($data)) {
$data = [];
}
$json = json_encode($data);
$connection->executeStatement('UPDATE kimai2_users SET roles = :data WHERE id = :id', [
'data' => $json,
'id' => $id,
]);
}
$table = $schema->getTable('kimai2_users');
$table->dropColumn('api_token');
$column = $table->getColumn('roles');
$column->setType(Type::getType(Types::JSON));
$column->setComment('(DC2Type:json)');
$column->setNotnull(true);
$table = $schema->getTable('kimai2_timesheet');
$table->dropColumn('category');
$this->addSql("DELETE FROM kimai2_user_preferences WHERE `name` = 'export_decimal'");
}
public function down(Schema $schema): void
{
$connection = $this->connection;
// this will leave behind a json_valid() check in mariadb
$connection->executeStatement('ALTER TABLE kimai2_users CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\'');
// Fetch the existing rows from the table
$rows = $connection->fetchAllAssociative('SELECT id, roles FROM kimai2_users');
// Iterate over each row
foreach ($rows as $row) {
$id = $row['id'];
$roles = $row['roles'];
$data = [];
if (json_validate($roles)) {
$data = json_decode($roles, true);
}
if (!\is_array($data)) {
$data = [];
}
$connection->executeStatement('UPDATE kimai2_users SET roles = :roles WHERE id = :id', [
'roles' => serialize($data),
'id' => $id,
]);
}
$timesheetTable = $schema->getTable('kimai2_timesheet');
$timesheetTable->addColumn('category', 'string', ['length' => 10, 'notnull' => true, 'default' => 'work']);
$usersTable = $schema->getTable('kimai2_users');
$usersTable->addColumn('api_token', 'string', ['length' => 255, 'notnull' => false, 'default' => null]);
}
public function isTransactional(): bool
{
return false;
}
}

View File

@@ -1,59 +1,58 @@
{
"name": "kimai",
"homepage": "https://github.com/kimai/kimai",
"license": "AGPL",
"private": true,
"maintainers": [
{
"name": "Kevin Papst",
"url": "https://www.kevinpapst.de"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/kimai/kimai.git"
},
"scripts": {
"dev-server": "yarn encore dev-server",
"dev": "yarn encore dev",
"watch": "yarn encore dev --watch",
"lint": "yarn eslint assets/js/",
"build": "yarn encore production"
},
"browserslist": [
"defaults"
],
"dependencies": {
"@babel/core": "^7",
"@babel/eslint-parser": "^7",
"@babel/plugin-syntax-dynamic-import": "^7",
"@babel/preset-env": "^7",
"@eslint/js": "^9",
"@fortawesome/fontawesome-free": "^6",
"@fullcalendar/bootstrap5": "^5",
"@fullcalendar/core": "^5",
"@fullcalendar/daygrid": "^5",
"@fullcalendar/google-calendar": "^5",
"@fullcalendar/icalendar": "^5",
"@fullcalendar/interaction": "^5",
"@fullcalendar/timegrid": "^5",
"@popperjs/core": "^2",
"@symfony/webpack-encore": "^5",
"@tabler/core": "1.4",
"bootstrap": "^5.3",
"chart.js": "^4",
"core-js": "^3",
"eslint": "^9",
"globals": "^15",
"gridstack": "^7",
"highlight.js": "^11",
"litepicker": "^2",
"luxon": "^3",
"sass": "^1",
"sass-loader": "^16",
"tom-select": "^2",
"webpack": "^5",
"webpack-cli": "^5"
},
"packageManager": "yarn@4.5.3"
"name": "kimai",
"homepage": "https://github.com/kimai/kimai",
"license": "AGPL",
"maintainers": [
{
"name": "Kevin Papst",
"url": "https://www.kevinpapst.de"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/kimai/kimai.git"
},
"scripts": {
"dev-server": "yarn encore dev-server",
"dev": "yarn encore dev",
"watch": "yarn encore dev --watch",
"lint": "yarn eslint assets/js/",
"build": "yarn encore production"
},
"browserslist": [
"defaults"
],
"dependencies": {
"@babel/core": "^7",
"@babel/eslint-parser": "^7",
"@babel/plugin-syntax-dynamic-import": "^7",
"@babel/preset-env": "^7",
"@eslint/js": "^9",
"@fortawesome/fontawesome-free": "^6",
"@fullcalendar/bootstrap5": "^5",
"@fullcalendar/core": "^5",
"@fullcalendar/daygrid": "^5",
"@fullcalendar/google-calendar": "^5",
"@fullcalendar/icalendar": "^5",
"@fullcalendar/interaction": "^5",
"@fullcalendar/timegrid": "^5",
"@popperjs/core": "^2",
"@symfony/webpack-encore": "^5",
"@tabler/core": "1.4",
"bootstrap": "^5.3",
"chart.js": "^4",
"core-js": "^3",
"eslint": "^9",
"globals": "^15",
"gridstack": "^7",
"highlight.js": "^11.11.1",
"litepicker": "^2",
"luxon": "^3",
"sass": "^1",
"sass-loader": "^16",
"tom-select": "^2.4.3",
"webpack": "^5",
"webpack-cli": "^5"
},
"packageManager": "yarn@4.5.3"
}

View File

@@ -27,6 +27,7 @@ parameters:
numericOperandsInArithmeticOperators: true
switchConditionsMatchingType: true
noVariableVariables: false
reportNonIntStringArrayKey: false
paths:
- src
tmpDir: %rootDir%/../../../var/cache/phpstan
@@ -49,6 +50,16 @@ parameters:
count: 1
path: src/API/ActivityController.php
-
message: "#^Method App\\\\API\\\\Authentication\\\\TokenAuthenticator\\:\\:getCredentials\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/API/Authentication/TokenAuthenticator.php
-
message: "#^Parameter \\#2 \\$plainPassword of method Symfony\\\\Component\\\\PasswordHasher\\\\PasswordHasherInterface\\:\\:verify\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: src/API/Authentication/TokenAuthenticator.php
-
message: "#^Parameter \\#1 \\$name of method App\\\\Entity\\\\Customer\\:\\:getMetaField\\(\\) expects string, mixed given\\.$#"
count: 1
@@ -169,6 +180,11 @@ parameters:
count: 1
path: src/Command/AbstractUserCommand.php
-
message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|null given\\.$#"
count: 1
path: src/Command/AbstractUserCommand.php
-
message: "#^Parameter \\#1 \\$username of method App\\\\User\\\\UserService\\:\\:findUserByUsernameOrThrowException\\(\\) expects string, mixed given\\.$#"
count: 1
@@ -389,6 +405,11 @@ parameters:
count: 1
path: src/Command/PromoteUserCommand.php
-
message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#"
count: 1
path: src/Command/RegenerateLocalesCommand.php
-
message: "#^Cannot call method find\\(\\) on Symfony\\\\Component\\\\Console\\\\Application\\|null\\.$#"
count: 4
@@ -574,6 +595,21 @@ parameters:
count: 1
path: src/Configuration/SystemConfiguration.php
-
message: "#^Method App\\\\Configuration\\\\SystemConfiguration\\:\\:offsetExists\\(\\) has parameter \\$offset with no type specified\\.$#"
count: 1
path: src/Configuration/SystemConfiguration.php
-
message: "#^Method App\\\\Configuration\\\\SystemConfiguration\\:\\:offsetGet\\(\\) has parameter \\$offset with no type specified\\.$#"
count: 1
path: src/Configuration/SystemConfiguration.php
-
message: "#^Parameter \\#1 \\$key of method App\\\\Configuration\\\\SystemConfiguration\\:\\:set\\(\\) expects string, mixed given\\.$#"
count: 1
path: src/Configuration/SystemConfiguration.php
-
message: "#^Parameter \\#1 \\$string of function trim expects string, bool\\|float\\|int\\|string given\\.$#"
count: 1
@@ -1139,11 +1175,21 @@ parameters:
count: 1
path: src/Entity/Team.php
-
message: "#^Method App\\\\Entity\\\\Timesheet\\:\\:getCategory\\(\\) should return string but returns string\\|null\\.$#"
count: 1
path: src/Entity/Timesheet.php
-
message: "#^Method App\\\\Entity\\\\Timesheet\\:\\:getMetaFields\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
count: 1
path: src/Entity/Timesheet.php
-
message: "#^Method App\\\\Entity\\\\Timesheet\\:\\:getTags\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
count: 1
path: src/Entity/Timesheet.php
-
message: "#^Parameter \\#1 \\$name of method App\\\\Entity\\\\Timesheet\\:\\:getMetaField\\(\\) expects string, string\\|null given\\.$#"
count: 1
@@ -1219,6 +1265,21 @@ parameters:
count: 1
path: src/Entity/User.php
-
message: "#^Method App\\\\Entity\\\\User\\:\\:setConfirmationToken\\(\\) has parameter \\$confirmationToken with no type specified\\.$#"
count: 1
path: src/Entity/User.php
-
message: "#^Method App\\\\Entity\\\\User\\:\\:setPassword\\(\\) has parameter \\$password with no type specified\\.$#"
count: 1
path: src/Entity/User.php
-
message: "#^Method App\\\\Entity\\\\User\\:\\:setPlainPassword\\(\\) has parameter \\$password with no type specified\\.$#"
count: 1
path: src/Entity/User.php
-
message: "#^Method App\\\\Entity\\\\User\\:\\:setRoles\\(\\) has parameter \\$roles with no value type specified in iterable type array\\.$#"
count: 1
@@ -1304,6 +1365,11 @@ parameters:
count: 1
path: src/Event/RevenueStatisticEvent.php
-
message: "#^Method App\\\\Event\\\\ThemeJavascriptTranslationsEvent\\:\\:getTranslations\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Event/ThemeJavascriptTranslationsEvent.php
-
message: "#^Parameter \\#1 \\$string of function strtolower expects string, string\\|null given\\.$#"
count: 2
@@ -1364,6 +1430,16 @@ parameters:
count: 1
path: src/Export/Annotation/Order.php
-
message: "#^Method App\\\\Export\\\\Base\\\\HtmlRenderer\\:\\:getOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Export/Base/HtmlRenderer.php
-
message: "#^Method App\\\\Export\\\\Base\\\\PDFRenderer\\:\\:getOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Export/Base/PDFRenderer.php
-
message: "#^Method App\\\\Export\\\\Base\\\\PDFRenderer\\:\\:getPdfOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
@@ -1864,6 +1940,61 @@ parameters:
count: 1
path: src/Form/TimesheetPreCreateForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addActivityMultiChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addActivitySelect\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addCustomerMultiChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addCustomerSelect\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addDateRange\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addOrderBy\\(\\) has parameter \\$allowedColumns with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addProjectMultiChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addProjectSelect\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addTeamsChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addUsersChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Parameter \\#1 \\$user of method App\\\\Repository\\\\Query\\\\BaseFormTypeQuery\\:\\:setUser\\(\\) expects App\\\\Entity\\\\User, mixed given\\.$#"
count: 2
path: src/Form/Toolbar/AbstractToolbarForm.php
-
message: "#^Method App\\\\Form\\\\Toolbar\\\\ActivityToolbarForm\\:\\:addActivityMultiChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
@@ -3191,7 +3322,7 @@ parameters:
-
message: "#^Parameter \\#1 \\$identifier of method App\\\\Entity\\\\User\\:\\:setUserIdentifier\\(\\) expects string, string\\|null given\\.$#"
count: 1
count: 2
path: src/Saml/SamlProvider.php
-
@@ -3199,6 +3330,16 @@ parameters:
count: 1
path: src/Saml/SamlProvider.php
-
message: "#^Property App\\\\Saml\\\\Security\\\\SamlAuthenticationFailureHandler\\:\\:\\$defaultOptions has no type specified\\.$#"
count: 1
path: src/Saml/Security/SamlAuthenticationFailureHandler.php
-
message: "#^Property App\\\\Saml\\\\Security\\\\SamlAuthenticationSuccessHandler\\:\\:\\$defaultOptions has no type specified\\.$#"
count: 1
path: src/Saml/Security/SamlAuthenticationSuccessHandler.php
-
message: "#^Method App\\\\Security\\\\KimaiUserProvider\\:\\:getProviders\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
@@ -3335,24 +3476,34 @@ parameters:
path: src/Twig/Context.php
-
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:checkInColumDefinition\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:checkInColumDefinition\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
count: 1
path: src/Twig/Runtime/DatatableExtensions.php
path: src/Twig/DatatableExtensions.php
-
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:getDatatableColumnClass\\(\\) should return string but returns bool\\|string\\.$#"
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:getClass\\(\\) has parameter \\$class with no type specified\\.$#"
count: 1
path: src/Twig/Runtime/DatatableExtensions.php
path: src/Twig/DatatableExtensions.php
-
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) has parameter \\$defaultColumns with no value type specified in iterable type array\\.$#"
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:getDatatableColumnClass\\(\\) should return string but returns bool\\|string\\.$#"
count: 1
path: src/Twig/Runtime/DatatableExtensions.php
path: src/Twig/DatatableExtensions.php
-
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) return type has no value type specified in iterable type array\\.$#"
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) has parameter \\$defaultColumns with no value type specified in iterable type array\\.$#"
count: 1
path: src/Twig/Runtime/DatatableExtensions.php
path: src/Twig/DatatableExtensions.php
-
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Twig/DatatableExtensions.php
-
message: "#^Property App\\\\Twig\\\\DatatableExtensions\\:\\:\\$tableNames type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Twig/DatatableExtensions.php
-
message: "#^Method App\\\\Twig\\\\Extensions\\:\\:replaceNewline\\(\\) has no return type specified\\.$#"
@@ -3389,6 +3540,21 @@ parameters:
count: 1
path: src/Twig/LocaleFormatExtensions.php
-
message: "#^Method App\\\\Twig\\\\PaginationExtension\\:\\:createRouteGenerator\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Twig/PaginationExtension.php
-
message: "#^Method App\\\\Twig\\\\PaginationExtension\\:\\:renderPagination\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Twig/PaginationExtension.php
-
message: "#^Method App\\\\Twig\\\\PaginationExtension\\:\\:renderPagination\\(\\) has parameter \\$pager with generic class Pagerfanta\\\\Pagerfanta but does not specify its types\\: T$#"
count: 1
path: src/Twig/PaginationExtension.php
-
message: "#^Method App\\\\Twig\\\\Runtime\\\\ThemeExtension\\:\\:actions\\(\\) has parameter \\$payload with no value type specified in iterable type array\\.$#"
count: 1
@@ -3429,6 +3595,11 @@ parameters:
count: 3
path: src/Utils/Color.php
-
message: "#^Class App\\\\Utils\\\\DataTable implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
count: 1
path: src/Utils/DataTable.php
-
message: "#^Method App\\\\Utils\\\\DataTable\\:\\:addColumn\\(\\) has parameter \\$column with no value type specified in iterable type array\\.$#"
count: 1
@@ -3439,6 +3610,11 @@ parameters:
count: 1
path: src/Utils/DataTable.php
-
message: "#^Method App\\\\Utils\\\\DataTable\\:\\:getIterator\\(\\) should return Traversable\\<mixed, mixed\\> but returns Traversable\\<\\(int\\|string\\), mixed\\>\\|null\\.$#"
count: 1
path: src/Utils/DataTable.php
-
message: "#^Method App\\\\Utils\\\\DataTable\\:\\:getOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
@@ -3579,6 +3755,11 @@ parameters:
count: 1
path: src/Utils/PageSetup.php
-
message: "#^Class App\\\\Utils\\\\Pagination extends generic class Pagerfanta\\\\Pagerfanta but does not specify its types\\: T$#"
count: 1
path: src/Utils/Pagination.php
-
message: "#^Parameter \\#1 \\$class of method App\\\\Utils\\\\PaginationTemplate\\:\\:linkLi\\(\\) expects string, mixed given\\.$#"
count: 1

View File

@@ -1,12 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
backupGlobals="false"
colors="true"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
cacheDirectory="var/cache/phpunit/"
displayDetailsOnTestsThatTriggerDeprecations="true"
@@ -14,86 +10,59 @@
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnPhpunitDeprecations="true">
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1"/>
<ini name="max_execution_time" value="-1"/>
<ini name="date.timezone" value="UTC"/>
<ini name="intl.default_locale" value="en"/>
<ini name="date.timezone" value="Europe/Vienna"/>
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
<server name="SHELL_VERBOSITY" value="-1" />
<env name="APP_DEBUG" value="0" force="true"/>
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?charset=utf8mb4&amp;serverVersion=10.11.15-MariaDB"/>
<!-- ###+ nelmio/cors-bundle ### -->
<env name="CORS_ALLOW_ORIGIN" value="'^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'"/>
<!-- ###- nelmio/cors-bundle ### -->
<!--
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
<php>
<ini name="error_reporting" value="-1"/>
<ini name="max_execution_time" value="-1"/>
<ini name="date.timezone" value="UTC"/>
<ini name="intl.default_locale" value="en"/>
<ini name="date.timezone" value="Europe/Vienna"/>
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
<env name="APP_ENV" value="test" force="true"/>
<env name="APP_DEBUG" value="0" force="true"/>
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?charset=utf8mb4&amp;serverVersion=10.5.8-MariaDB"/>
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
<env name="MAILER_URL" value="null://null"/>
<env name="MAILER_FROM" value="kimai@example.com"/>
<!--
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
- change to "true"
- composer tests
- change back to "false"
- change to "true"
- composer tests
- change back to "false"
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
-->
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
<!-- ###+ symfony/framework-bundle ### -->
<env name="APP_ENV" value="test" force="true"/>
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
<env name="APP_SHARE_DIR" value="var/share"/>
<!-- ###- symfony/framework-bundle ### -->
<!-- ###+ symfony/mailer ### -->
<env name="MAILER_URL" value="null://null"/>
<env name="MAILER_FROM" value="kimai@example.com"/>
<!-- ###- symfony/mailer ### -->
<!-- ###+ symfony/routing ### -->
<!-- Configure how to generate URLs in non-HTTP contexts, such as CLI commands. -->
<!-- See https://symfony.com/doc/current/routing.html#generating-urls-in-commands -->
<env name="DEFAULT_URI" value="http://localhost"/>
<!-- ###- symfony/routing ### -->
<!-- ###+ symfony/messenger ### -->
<!-- Choose one of the transports below -->
<!-- MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages -->
<!-- MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages -->
<env name="MESSENGER_TRANSPORT_DSN" value="doctrine://default?auto_setup=0"/>
<!-- ###- symfony/messenger ### -->
</php>
<testsuites>
<testsuite name="Kimai">
<directory>tests/</directory>
</testsuite>
</testsuites>
<extensions>
<bootstrap class="Symfony\Bridge\PhpUnit\SymfonyExtension">
<parameter name="clock-mock-namespaces" value="App" />
<parameter name="dns-mock-namespaces" value="App" />
</bootstrap>
<bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>
<source ignoreSuppressionOfDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory suffix=".php">src/</directory>
<directory suffix=".php">templates/</directory>
</include>
<exclude>
<directory suffix=".php">migrations/</directory>
<directory suffix=".php">assets/</directory>
<directory suffix=".php">bin/</directory>
<directory suffix=".php">config/</directory>
<directory suffix=".php">node_modules/</directory>
<directory suffix=".php">public/</directory>
<directory suffix=".php">tests/</directory>
<directory suffix=".php">translations/</directory>
<directory suffix=".php">var/</directory>
<directory suffix=".php">vendor/</directory>
</exclude>
</source>
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
-->
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
</php>
<testsuites>
<testsuite name="Kimai">
<directory>tests/</directory>
</testsuite>
</testsuites>
<extensions>
<bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>
<source>
<include>
<directory suffix=".php">src/</directory>
<directory suffix=".php">templates/</directory>
</include>
<exclude>
<directory suffix=".php">migrations/</directory>
<directory suffix=".php">assets/</directory>
<directory suffix=".php">bin/</directory>
<directory suffix=".php">config/</directory>
<directory suffix=".php">node_modules/</directory>
<directory suffix=".php">public/</directory>
<directory suffix=".php">tests/</directory>
<directory suffix=".php">translations/</directory>
<directory suffix=".php">var/</directory>
<directory suffix=".php">vendor/</directory>
</exclude>
</source>
</phpunit>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
/*!
* @kurkle/color v0.3.2
* @kurkle/color v0.3.4
* https://github.com/kurkle/color#readme
* (c) 2023 Jukka Kurkela
* (c) 2024 Jukka Kurkela
* Released under the MIT License
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,25 +2,25 @@
"entrypoints": {
"app": {
"js": [
"/build/runtime.6c399d29.js",
"/build/app.3ba0bd00.js"
"/build/runtime.684e9f6d.js",
"/build/app.41748d55.js"
],
"css": [
"/build/app.16ea604e.css"
"/build/app.6bfb9c66.css"
]
},
"app-rtl": {
"js": [
"/build/runtime.6c399d29.js",
"/build/runtime.684e9f6d.js",
"/build/app-rtl.15853b82.js"
],
"css": [
"/build/app-rtl.f149299b.css"
"/build/app-rtl.16271884.css"
]
},
"export-pdf": {
"js": [
"/build/runtime.6c399d29.js",
"/build/runtime.684e9f6d.js",
"/build/export-pdf.395749ab.js"
],
"css": [
@@ -29,7 +29,7 @@
},
"invoice": {
"js": [
"/build/runtime.6c399d29.js",
"/build/runtime.684e9f6d.js",
"/build/invoice.42b319e4.js"
],
"css": [
@@ -38,7 +38,7 @@
},
"invoice-pdf": {
"js": [
"/build/runtime.6c399d29.js",
"/build/runtime.684e9f6d.js",
"/build/invoice-pdf.26d98626.js"
],
"css": [
@@ -47,14 +47,14 @@
},
"chart": {
"js": [
"/build/runtime.6c399d29.js",
"/build/chart.bafa38e7.js"
"/build/runtime.684e9f6d.js",
"/build/chart.56f16a68.js"
]
},
"calendar": {
"js": [
"/build/runtime.6c399d29.js",
"/build/calendar.4242f779.js"
"/build/runtime.684e9f6d.js",
"/build/calendar.36a92145.js"
],
"css": [
"/build/calendar.d757753e.css"
@@ -62,8 +62,8 @@
},
"dashboard": {
"js": [
"/build/runtime.6c399d29.js",
"/build/dashboard.6ce7ac9c.js"
"/build/runtime.684e9f6d.js",
"/build/dashboard.9708ae5e.js"
],
"css": [
"/build/dashboard.b7129fa1.css"
@@ -71,8 +71,8 @@
},
"highlight": {
"js": [
"/build/runtime.6c399d29.js",
"/build/highlight.13d5d50e.js"
"/build/runtime.684e9f6d.js",
"/build/highlight.0296a734.js"
],
"css": [
"/build/highlight.98bf3927.css"
@@ -80,23 +80,23 @@
}
},
"integrity": {
"/build/runtime.6c399d29.js": "sha384-/rm616f12czi8l/27GvWXtb3g608vJZf2XTUKxqCRI4tsa2vUHP+BW90edTok5zC",
"/build/app.3ba0bd00.js": "sha384-4G73IK+GVsttYdvW0Pt2Hov27rTJuGDusRBvoRnzPezDlTiArF4MV9mVDi7anlX+",
"/build/app.16ea604e.css": "sha384-9734FGgqgVw+kNVry5jl15+pbaetOtfl3eywAy2pSigGpeIXlCh1udhh7rI9lNKo",
"/build/runtime.684e9f6d.js": "sha384-suKiEX2de4fdNqQzdYbUd6osp4AepD9FiMXl+1QdvgMW9dcQqUWQNQasf3KWzwLr",
"/build/app.41748d55.js": "sha384-iVPHnOXYjVDuq8567h8I3U2gl5SCV3mNMFNQCJdgMiNuFMZ4KO7PXOLanjgcGzTt",
"/build/app.6bfb9c66.css": "sha384-38+zSolYBrZdSwNcrDi5qCaQGCQlBJRKwtsJV+pnrf8rCCpf6z/M7fEpV4tS79Sz",
"/build/app-rtl.15853b82.js": "sha384-UnKKgLMu9FnRT+CFE0no/+UiUks012bYriQdUWa6f02mo6Lswl947mPybjvKL503",
"/build/app-rtl.f149299b.css": "sha384-nCTYbFHUkkF20RA5oXqGah3iDnnSUxAmuFHpbehTzHszCgJBVRoHC81Q5OwFreLf",
"/build/app-rtl.16271884.css": "sha384-P1x4vYfjfM4JL55NQ3sfLiy+BSy8cM4bNcshj9M62nMJ4JD2xAWUioNv6VrmaK7U",
"/build/export-pdf.395749ab.js": "sha384-3Hjvmu4FC/0dhHnR8kyRBU7k2xMNy1lxBpGgOkrw8PxXnwyQDM8/5bQmkJbjVT1+",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.42b319e4.js": "sha384-xxK7sCe/ZhTjMPFPeX1xvILURxNRZz2hJHZxAGVaw9zE6TC++2/6y2eKqg8cz852",
"/build/invoice.36018785.css": "sha384-jukM9uZ6pexDxXKgZThSxiqXimzsxzniBMHz08N9x8ryXZYkM5r/ZgaainCV0+J6",
"/build/invoice-pdf.26d98626.js": "sha384-gwNzQiU1y6qU/M9DPGiNW0MVZkLctEHk37sCES2X9ov+zugEaDABdkMjKBYOC9lz",
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
"/build/chart.bafa38e7.js": "sha384-Ays2qGKvOqs4NSeN/zJOPcrnzIEC/uYSCIkC3kN0KQye+mA5Lq7UkfjZDjulvFUX",
"/build/calendar.4242f779.js": "sha384-fz1ouLGHV92+3gq3eWYAgEIbwVSmuCfILeVjAnnXGjJJfUtMs+qlGEIIo3uQa0rz",
"/build/chart.56f16a68.js": "sha384-SWnYjAbZ8OWEvTP+IZfGuBMWJIcH9OZWLYQ4p38KMPmqfufsc2zhzqWkALO3mCBO",
"/build/calendar.36a92145.js": "sha384-ZPLxxbep6OnPeUiIeBXRE1iscZFgbNuTpn3rx2Jpf1KR2l6sxIuNsieDUskbmQ3m",
"/build/calendar.d757753e.css": "sha384-cTmQMgHYjd2gfObFWmEUph7qQLCyXaIkneSf+bQ2mqVmZwqOB+pJOm/UYTyTjALJ",
"/build/dashboard.6ce7ac9c.js": "sha384-5BOoyjZx/ZKr9IbogWfT/aBRHx7XA1fePADdUpUNlSF/rqVo3taXLLcrIoaDZ+8b",
"/build/dashboard.9708ae5e.js": "sha384-QN7XIQuxFZu76sHVrgdpZL81+Q2VTwcgF3aI2CnpwYZiMoPbzrcTGfPL3RDy696t",
"/build/dashboard.b7129fa1.css": "sha384-2nn5hLA+3YedgHYBpge62S8Losj8aoPwK9Zk9EvN1xEYatvOUQ7H3rIR2UUJAGOS",
"/build/highlight.13d5d50e.js": "sha384-+RqBBPIzp5YQpbBeHgVNXzHiewK1PZtizPh4in1PlBU6RcT1bDleW3QSJMDeBnPO",
"/build/highlight.0296a734.js": "sha384-vGFKI/KM+uyGIlzKsKhQh4w41hQteB4u14oRYqObMT03/YTZXq6Re5nUN6g3sGQH",
"/build/highlight.98bf3927.css": "sha384-YgweSwDwN0dI4DEmh478xYVw/TewJYvCO1QTbWHpaHFDPuLrZvYMR0Tc+QfFxVPE"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{
"build/app.css": "/build/app.16ea604e.css",
"build/app.js": "/build/app.3ba0bd00.js",
"build/app-rtl.css": "/build/app-rtl.f149299b.css",
"build/app.css": "/build/app.6bfb9c66.css",
"build/app.js": "/build/app.41748d55.js",
"build/app-rtl.css": "/build/app-rtl.16271884.css",
"build/app-rtl.js": "/build/app-rtl.15853b82.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.395749ab.js",
@@ -9,14 +9,14 @@
"build/invoice.js": "/build/invoice.42b319e4.js",
"build/invoice-pdf.css": "/build/invoice-pdf.2b749265.css",
"build/invoice-pdf.js": "/build/invoice-pdf.26d98626.js",
"build/chart.js": "/build/chart.bafa38e7.js",
"build/chart.js": "/build/chart.56f16a68.js",
"build/calendar.css": "/build/calendar.d757753e.css",
"build/calendar.js": "/build/calendar.4242f779.js",
"build/calendar.js": "/build/calendar.36a92145.js",
"build/dashboard.css": "/build/dashboard.b7129fa1.css",
"build/dashboard.js": "/build/dashboard.6ce7ac9c.js",
"build/dashboard.js": "/build/dashboard.9708ae5e.js",
"build/highlight.css": "/build/highlight.98bf3927.css",
"build/highlight.js": "/build/highlight.13d5d50e.js",
"build/runtime.js": "/build/runtime.6c399d29.js",
"build/highlight.js": "/build/highlight.0296a734.js",
"build/runtime.js": "/build/runtime.684e9f6d.js",
"build/fonts/fa-solid-900.ttf": "/build/fonts/fa-solid-900.2582b0e4.ttf",
"build/fonts/fa-brands-400.ttf": "/build/fonts/fa-brands-400.1815e004.ttf",
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.2463b90d.woff2",

View File

@@ -0,0 +1 @@
!function(){"use strict";var e,r={},n={};function t(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={id:e,loaded:!1,exports:{}};return r[e].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}t.m=r,t.amdO={},e=[],t.O=function(r,n,o,i){if(!n){var u=1/0;for(l=0;l<e.length;l++){n=e[l][0],o=e[l][1],i=e[l][2];for(var f=!0,a=0;a<n.length;a++)(!1&i||u>=i)&&Object.keys(t.O).every(function(e){return t.O[e](n[a])})?n.splice(a--,1):(f=!1,i<u&&(u=i));if(f){e.splice(l--,1);var c=o();void 0!==c&&(r=c)}}return r}i=i||0;for(var l=e.length;l>0&&e[l-1][2]>i;l--)e[l]=e[l-1];e[l]=[n,o,i]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e={121:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var o,i,u=n[0],f=n[1],a=n[2],c=0;if(u.some(function(r){return 0!==e[r]})){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(a)var l=a(t)}for(r&&r(n);c<u.length;c++)i=u[c],t.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return t.O(l)},n=self.webpackChunkkimai=self.webpackChunkkimai||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))}()}();

View File

@@ -1 +0,0 @@
!function(){"use strict";var e,r={},n={};function t(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={id:e,loaded:!1,exports:{}};return r[e].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}t.m=r,t.amdO={},e=[],t.O=function(r,n,o,i){if(!n){var u=1/0;for(l=0;l<e.length;l++){n=e[l][0],o=e[l][1],i=e[l][2];for(var f=!0,a=0;a<n.length;a++)(!1&i||u>=i)&&Object.keys(t.O).every((function(e){return t.O[e](n[a])}))?n.splice(a--,1):(f=!1,i<u&&(u=i));if(f){e.splice(l--,1);var c=o();void 0!==c&&(r=c)}}return r}i=i||0;for(var l=e.length;l>0&&e[l-1][2]>i;l--)e[l]=e[l-1];e[l]=[n,o,i]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e={121:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var o,i,u=n[0],f=n[1],a=n[2],c=0;if(u.some((function(r){return 0!==e[r]}))){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(a)var l=a(t)}for(r&&r(n);c<u.length;c++)i=u[c],t.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return t.O(l)},n=self.webpackChunkkimai=self.webpackChunkkimai||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))}()}();

View File

@@ -35,9 +35,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Activity')]
final class ActivityController extends BaseApiController
{
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Activity', 'Activity_Entity'];
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Activity'];
private const array GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Activity', 'Activity_Entity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Activity'];
public const GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
public function __construct(
private readonly ViewHandlerInterface $viewHandler,

View File

@@ -26,12 +26,22 @@ final class ApiRequestMatcher implements RequestMatcherInterface
return false;
}
// ------------------------------------------------------------------------------------
// the next two checks are primarily here to make sure to return proper error messages
// let's use this firewall if a Bearer token is set in the header
// other cases like "bearer" are rejected earlier
if (($auth = $request->headers->get('Authorization')) !== null && str_starts_with($auth, 'Bearer ')) {
return true;
}
// let's use this firewall if the deprecated username & token combination is available
if ($request->headers->has(TokenAuthenticator::HEADER_USERNAME) &&
$request->headers->has(TokenAuthenticator::HEADER_TOKEN)) {
return true;
}
// ------------------------------------------------------------------------------------
// checking for a previous session allows us to skip the API firewall and token access handler
// we simply re-use the existing session when doing API calls from the frontend.
// it is not necessary to check headers. if there is no valid session, we should always use this firewall

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\API\Authentication;
use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
final class ApiTokenMigratingListener implements EventSubscriberInterface
{
public function __construct(private PasswordHasherFactoryInterface $hasherFactory)
{
}
public function onLoginSuccess(LoginSuccessEvent $event): void
{
$passport = $event->getPassport();
if (!$passport->hasBadge(ApiTokenUpgradeBadge::class)) {
return;
}
/** @var ApiTokenUpgradeBadge $badge */
$badge = $passport->getBadge(ApiTokenUpgradeBadge::class);
$plaintextApiToken = $badge->getAndErasePlaintextApiToken();
if ('' === $plaintextApiToken) {
return;
}
$user = $passport->getUser();
if (!($user instanceof User)) {
return;
}
if (null === $user->getApiToken()) {
return;
}
$passwordHasher = $this->hasherFactory->getPasswordHasher($user);
if (!$passwordHasher->needsRehash($user->getApiToken())) {
return;
}
$badge->getPasswordUpgrader()->upgradePassword($user, $passwordHasher->hash($plaintextApiToken));
}
public static function getSubscribedEvents(): array
{
return [LoginSuccessEvent::class => 'onLoginSuccess'];
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\API\Authentication;
use Symfony\Component\Security\Core\Exception\LogicException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
final class ApiTokenUpgradeBadge implements BadgeInterface
{
public function __construct(private ?string $plaintextApiToken, private readonly PasswordUpgraderInterface $passwordUpgrader)
{
}
public function getAndErasePlaintextApiToken(): string
{
$password = $this->plaintextApiToken;
if (null === $password) {
throw new LogicException('The api token is erased as another listener already used this badge.');
}
$this->plaintextApiToken = null;
return $password;
}
public function getPasswordUpgrader(): PasswordUpgraderInterface
{
return $this->passwordUpgrader;
}
public function isResolved(): bool
{
return true;
}
}

View File

@@ -0,0 +1,117 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\API\Authentication;
use App\Entity\User;
use App\Repository\ApiUserRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
final class TokenAuthenticator extends AbstractAuthenticator
{
public const HEADER_USERNAME = 'X-AUTH-USER';
public const HEADER_TOKEN = 'X-AUTH-TOKEN';
public function __construct(
private readonly ApiUserRepository $userProvider,
private readonly PasswordHasherFactoryInterface $passwordHasherFactory
)
{
}
public function supports(Request $request): bool
{
if (str_contains($request->getRequestUri(), '/api/')) {
if (str_contains($request->getRequestUri(), '/api/doc')) {
return false;
}
if ($request->headers->has(self::HEADER_USERNAME) && $request->headers->has(self::HEADER_TOKEN)) {
@trigger_error('You are using deprecated API access, please upgrade your APP to use API tokens instead.', E_USER_DEPRECATED);
return true;
}
}
return false;
}
private function getCredentials(Request $request): array
{
$apiUser = $request->headers->get(self::HEADER_USERNAME);
if (null === $apiUser || '' === $apiUser) {
throw new CustomUserMessageAuthenticationException('Authentication required, missing user header: ' . self::HEADER_USERNAME);
}
$apiToken = $request->headers->get(self::HEADER_TOKEN);
if (null === $apiToken || '' === $apiToken) {
throw new CustomUserMessageAuthenticationException('Authentication required, missing token header: ' . self::HEADER_TOKEN);
}
return [
'username' => $apiUser,
'password' => $apiToken
];
}
public function authenticate(Request $request): Passport
{
$credentials = $this->getCredentials($request);
$checkCredentials = function (?string $presentedPassword, User $user) {
if ('' === $presentedPassword) {
throw new BadCredentialsException('The presented password cannot be empty.');
}
if (null === $user->getApiToken()) {
throw new BadCredentialsException('The user has no activated API account.');
}
if ($this->passwordHasherFactory->getPasswordHasher($user)->verify($user->getApiToken(), $presentedPassword)) {
return true;
}
throw new BadCredentialsException('The presented password is invalid.');
};
$passport = new Passport(
new UserBadge($credentials['username'], [$this->userProvider, 'loadUserByIdentifier']),
new CustomCredentials($checkCredentials, $credentials['password'])
);
$passport->addBadge(new ApiTokenUpgradeBadge($credentials['password'], $this->userProvider));
return $passport;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
$data = [
'message' => $exception instanceof CustomUserMessageAuthenticationException ? $exception->getMessage() : 'Invalid credentials'
];
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
}

View File

@@ -35,9 +35,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Customer')]
final class CustomerController extends BaseApiController
{
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Customer', 'Customer_Entity'];
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Customer'];
private const array GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Customer', 'Customer_Entity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Customer'];
public const GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
public function __construct(
private readonly ViewHandlerInterface $viewHandler,

View File

@@ -28,8 +28,8 @@ use Symfony\Component\Validator\Constraints;
#[OA\Tag(name: 'Invoice')]
final class InvoiceController extends BaseApiController
{
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Invoice', 'Invoice_Entity'];
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Invoice'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Invoice', 'Invoice_Entity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Invoice'];
public function __construct(
private readonly ViewHandlerInterface $viewHandler,

View File

@@ -37,9 +37,9 @@ use Symfony\Component\Validator\Constraints;
#[OA\Tag(name: 'Project')]
final class ProjectController extends BaseApiController
{
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Project', 'Project_Entity'];
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Project'];
private const array GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Project', 'Project_Entity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Project'];
public const GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
public function __construct(
private readonly ViewHandlerInterface $viewHandler,

View File

@@ -24,40 +24,44 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Default')]
final class StatusController extends BaseApiController
{
public function __construct(private readonly ViewHandlerInterface $viewHandler)
{
}
/**
* Testing route for the API
*/
#[OA\Response(response: 200, description: "A simple route that returns a 'pong', which you can use for testing the API", content: new OA\JsonContent(example: "{'message': 'pong'}"))]
#[Route(path: '/ping', methods: ['GET'])]
public function pingAction(ViewHandlerInterface $viewHandler): Response
#[Route(methods: ['GET'], path: '/ping')]
public function pingAction(): Response
{
$view = new View(['message' => 'pong'], 200);
return $viewHandler->handle($view);
return $this->viewHandler->handle($view);
}
/**
* Fetch Kimai release
*/
#[OA\Response(response: 200, description: 'Returns version information about the current release', content: new OA\JsonContent(ref: new Model(type: Version::class)))]
#[Route(path: '/version', methods: ['GET'])]
public function versionAction(ViewHandlerInterface $viewHandler): Response
#[Route(methods: ['GET'], path: '/version')]
public function versionAction(): Response
{
return $viewHandler->handle(new View(new Version(), 200));
return $this->viewHandler->handle(new View(new Version(), 200));
}
/**
* Fetch installed Plugins
*/
#[OA\Response(response: 200, description: 'Returns a list of plugin names and versions', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: new Model(type: Plugin::class))))]
#[Route(path: '/plugins', methods: ['GET'])]
public function pluginAction(PluginManager $pluginManager, ViewHandlerInterface $viewHandler): Response
#[Route(methods: ['GET'], path: '/plugins')]
public function pluginAction(PluginManager $pluginManager): Response
{
$plugins = [];
foreach ($pluginManager->getPlugins() as $plugin) {
$plugins[] = new Plugin($plugin);
}
return $viewHandler->handle(new View($plugins, 200));
return $this->viewHandler->handle(new View($plugins, 200));
}
}

View File

@@ -27,9 +27,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Tag')]
final class TagController extends BaseApiController
{
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Tag'];
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Tag'];
private const array GROUPS_FORM = ['Default', 'Entity', 'Tag'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Tag'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Tag'];
public const GROUPS_FORM = ['Default', 'Entity', 'Tag'];
public function __construct(
private readonly ViewHandlerInterface $viewHandler,
@@ -39,9 +39,9 @@ final class TagController extends BaseApiController
}
/**
* Fetch tags as strings
* Deprecated: Fetch tags as strings
*/
#[OA\Response(response: 200, description: 'Returns existing tags as string array', content: new OA\JsonContent(type: 'array', items: new OA\Items(type: 'string')))]
#[OA\Response(response: 200, description: 'DEPRECATED: Returns existing tags as string array', content: new OA\JsonContent(type: 'array', items: new OA\Items(type: 'string')))]
#[OA\Get(x: ['internal' => true])]
#[Route(methods: ['GET'], name: 'get_tags')]
#[Rest\QueryParam(name: 'name', strict: true, nullable: true, description: 'Search term to filter tag list')]

View File

@@ -36,9 +36,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Team')]
final class TeamController extends BaseApiController
{
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
private const array GROUPS_FORM = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Team'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
public const GROUPS_FORM = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Team'];
public function __construct(
private readonly ViewHandlerInterface $viewHandler,

View File

@@ -47,11 +47,11 @@ use Symfony\Component\Validator\Constraints;
#[OA\Tag(name: 'Timesheet')]
final class TimesheetController extends BaseApiController
{
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Not_Expanded'];
private const array GROUPS_ENTITY_FULL = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Expanded'];
private const array GROUPS_FORM = ['Default', 'Entity', 'Timesheet', 'Not_Expanded'];
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Timesheet', 'Not_Expanded'];
private const array GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'Timesheet', 'Expanded'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Not_Expanded'];
public const GROUPS_ENTITY_FULL = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Expanded'];
public const GROUPS_FORM = ['Default', 'Entity', 'Timesheet', 'Not_Expanded'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Timesheet', 'Not_Expanded'];
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'Timesheet', 'Expanded'];
public function __construct(
private readonly ViewHandlerInterface $viewHandler,

View File

@@ -38,10 +38,10 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'User')]
final class UserController extends BaseApiController
{
private const array GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
private const array GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
private const array GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'User', 'User_Entity'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
public const GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'User', 'User_Entity'];
public function __construct(
private readonly ViewHandlerInterface $viewHandler,
@@ -209,7 +209,7 @@ final class UserController extends BaseApiController
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Success if the token could be deleted.')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The API token ID to remove', required: true)]
#[Route(methods: ['DELETE'], path: '/api-token/{id}', name: 'delete_api_token', requirements: ['id' => '\d+'])]
public function deleteAccessToken(AccessToken $accessToken, AccessTokenRepository $accessTokenRepository): Response
public function deleteApiToken(AccessToken $accessToken, AccessTokenRepository $accessTokenRepository): Response
{
$user = $this->getUser();
if (!$this->isGranted('api-token', $user)) {

View File

@@ -63,13 +63,16 @@ class ActivityService
public function saveActivity(Activity $activity): Activity
{
if ($activity->isNew()) {
return $this->saveNewActivity($activity);
return $this->saveNewActivity($activity); // @phpstan-ignore method.deprecated
} else {
return $this->updateActivity($activity);
return $this->updateActivity($activity); // @phpstan-ignore method.deprecated
}
}
private function saveNewActivity(Activity $activity): Activity
/**
* @deprecated since 2.35 - use saveActivity() instead
*/
public function saveNewActivity(Activity $activity): Activity
{
if (null !== $activity->getId()) {
throw new InvalidArgumentException('Cannot create activity, already persisted');
@@ -103,7 +106,10 @@ class ActivityService
}
}
private function updateActivity(Activity $activity): Activity
/**
* @deprecated since 2.35 - use saveActivity() instead
*/
public function updateActivity(Activity $activity): Activity
{
$this->validateActivity($activity);

View File

@@ -9,22 +9,13 @@
namespace App\Audit;
/**
* For entity classes, whose changes could be logged.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Loggable
{
/**
* @param class-string|null $customFieldClass
* @param string[] $ignoredProperties
*/
public function __construct(
public ?string $customFieldClass = null,
public array $ignoredProperties = [],
public ?string $title = null,
public string $translationDomain = 'messages',
)
public function __construct(public ?string $customFieldClass = null)
{
}
}

View File

@@ -9,10 +9,10 @@
namespace App\Audit;
/**
* Marks a property as sensitive, so it will not be logged.
*/
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class SensitiveProperty
final class Versioned
{
public function __construct()
{
}
}

Some files were not shown because too many files have changed in this diff Show More