Release 2.0.19 (#4022)
* prevent invoices with exceeding filename * fixed invalid LDAP log level * support locale switching in action events
This commit is contained in:
@@ -25,8 +25,10 @@ and so much more.
|
|||||||
|
|
||||||
There are two versions of Kimai existing:
|
There are two versions of Kimai existing:
|
||||||
|
|
||||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — compatible with PHP 7.4, which is in maintenance mode since 2023
|
- [Version 2](https://github.com/kimai/kimai) — the current stable release (PHP 8.1 only)
|
||||||
- [Version 2](https://github.com/kimai/kimai) — stable and "almost released" (waiting for some major plugins, which are not yet migrated)
|
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — the "old" version, in maintenance mode since Jan. 2023 (PHP 7.4, PHP 8.1)
|
||||||
|
|
||||||
|
If you start fresh, do **not** use Version 1, it won't receive any more updates.
|
||||||
|
|
||||||
### Links
|
### Links
|
||||||
|
|
||||||
@@ -72,7 +74,7 @@ It is open for changes and input from the community, your [ideas and questions](
|
|||||||
Release versions will be created on a regular basis, every couple of weeks latest.
|
Release versions will be created on a regular basis, every couple of weeks latest.
|
||||||
Every code change, whether it's a new feature or a bugfix, will be done on the `main` branch.
|
Every code change, whether it's a new feature or a bugfix, will be done on the `main` branch.
|
||||||
|
|
||||||
For the time being and until 2.0 is widely adopted, the [1.x branch](https://github.com/kimai/kimai/tree/1.x) will receive bug fixes.
|
Until 2.0 is widely adopted, the [1.x branch](https://github.com/kimai/kimai/tree/1.x) will receive (critical) bug and security fixes.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
25
phpstan.neon
25
phpstan.neon
@@ -2266,11 +2266,6 @@ parameters:
|
|||||||
count: 9
|
count: 9
|
||||||
path: src/Event/PageActionsEvent.php
|
path: src/Event/PageActionsEvent.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Property App\\\\Event\\\\PageActionsEvent\\:\\:\\$divider has no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Event/PageActionsEvent.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Property App\\\\Event\\\\PermissionSectionsEvent\\:\\:\\$sections type has no value type specified in iterable type array\\.$#"
|
message: "#^Property App\\\\Event\\\\PermissionSectionsEvent\\:\\:\\$sections type has no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4531,11 +4526,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Invoice/Renderer/PdfRenderer.php
|
path: src/Invoice/Renderer/PdfRenderer.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Binary operation \"\\.\" between string and non\\-empty\\-list\\<string\\>\\|string results in an error\\.$#"
|
|
||||||
count: 2
|
|
||||||
path: src/Invoice/ServiceInvoice.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot call method getCustomer\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
message: "#^Cannot call method getCustomer\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4581,11 +4571,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Invoice/ServiceInvoice.php
|
path: src/Invoice/ServiceInvoice.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Invoice\\\\ServiceInvoice\\:\\:saveGeneratedInvoice\\(\\) should return string but returns array\\<int, string\\>\\|string\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Invoice/ServiceInvoice.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, int\\|null given\\.$#"
|
message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, int\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4601,11 +4586,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Invoice/ServiceInvoice.php
|
path: src/Invoice/ServiceInvoice.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$name of method Symfony\\\\Component\\\\HttpFoundation\\\\File\\\\File\\:\\:move\\(\\) expects string\\|null, array\\<int, string\\>\\|string given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Invoice/ServiceInvoice.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$string of function explode expects string, string\\|null given\\.$#"
|
message: "#^Parameter \\#2 \\$string of function explode expects string, string\\|null given\\.$#"
|
||||||
count: 2
|
count: 2
|
||||||
@@ -4621,11 +4601,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Invoice/ServiceInvoice.php
|
path: src/Invoice/ServiceInvoice.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, array\\<int, string\\>\\|string given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Invoice/ServiceInvoice.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Property App\\\\Invoice\\\\ServiceInvoice\\:\\:\\$invoiceItemRepositories type has no value type specified in iterable type array\\.$#"
|
message: "#^Property App\\\\Invoice\\\\ServiceInvoice\\:\\:\\$invoiceItemRepositories type has no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ final class ActionsController extends BaseApiController
|
|||||||
*/
|
*/
|
||||||
private function convertEvent(PageActionsEvent $event, string $locale): array
|
private function convertEvent(PageActionsEvent $event, string $locale): array
|
||||||
{
|
{
|
||||||
|
$event->setLocale($locale);
|
||||||
$this->dispatcher->dispatch($event, $event->getEventName());
|
$this->dispatcher->dispatch($event, $event->getEventName());
|
||||||
|
|
||||||
$translator = $this->translator;
|
$translator = $this->translator;
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.0.18';
|
public const VERSION = '2.0.19';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 20018;
|
public const VERSION_ID = 20019;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ use App\Entity\User;
|
|||||||
*/
|
*/
|
||||||
class PageActionsEvent extends ThemeEvent
|
class PageActionsEvent extends ThemeEvent
|
||||||
{
|
{
|
||||||
private $action;
|
private string $action;
|
||||||
private $view;
|
private string $view;
|
||||||
private $divider = 0;
|
private int $divider = 0;
|
||||||
|
private ?string $locale = null;
|
||||||
|
|
||||||
public function __construct(User $user, array $payload, string $action, string $view)
|
public function __construct(User $user, array $payload, string $action, string $view)
|
||||||
{
|
{
|
||||||
@@ -195,4 +196,14 @@ class PageActionsEvent extends ThemeEvent
|
|||||||
|
|
||||||
return \count($this->payload['actions']);
|
return \count($this->payload['actions']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLocale(): ?string
|
||||||
|
{
|
||||||
|
return $this->locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLocale(?string $locale): void
|
||||||
|
{
|
||||||
|
$this->locale = $locale;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,13 @@ class ThemeEvent extends Event
|
|||||||
return $this->payload;
|
return $this->payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated since 2.0.19, will be removed with 2.1
|
||||||
|
*/
|
||||||
public function setPayload(mixed $payload): void
|
public function setPayload(mixed $payload): void
|
||||||
{
|
{
|
||||||
|
@trigger_error('ThemeEvent::setPayload() is deprecated, use AbstractActionsSubscriber instead.', E_USER_DEPRECATED);
|
||||||
|
|
||||||
$this->payload = $payload;
|
$this->payload = $payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
|||||||
*/
|
*/
|
||||||
abstract class AbstractActionsSubscriber implements EventSubscriberInterface
|
abstract class AbstractActionsSubscriber implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
|
private ?string $locale = null;
|
||||||
|
|
||||||
public function __construct(private AuthorizationCheckerInterface $auth, private UrlGeneratorInterface $urlGenerator)
|
public function __construct(private AuthorizationCheckerInterface $auth, private UrlGeneratorInterface $urlGenerator)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -30,16 +32,27 @@ abstract class AbstractActionsSubscriber implements EventSubscriberInterface
|
|||||||
|
|
||||||
protected function path(string $route, array $parameters = []): string
|
protected function path(string $route, array $parameters = []): string
|
||||||
{
|
{
|
||||||
|
if ($this->locale !== null) {
|
||||||
|
$parameters['_locale'] = $this->locale;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->urlGenerator->generate($route, $parameters);
|
return $this->urlGenerator->generate($route, $parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
public static function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'actions.' . static::getActionName() => ['onActions', 1000],
|
'actions.' . static::getActionName() => ['handleEvent', 1000],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final public function handleEvent(PageActionsEvent $event): void
|
||||||
|
{
|
||||||
|
$this->locale = $event->getLocale();
|
||||||
|
|
||||||
|
$this->onActions($event);
|
||||||
|
}
|
||||||
|
|
||||||
public static function getActionName(): string
|
public static function getActionName(): string
|
||||||
{
|
{
|
||||||
throw new \Exception('You need to overwrite getActionName() or getSubscribedEvents() in ' . static::class);
|
throw new \Exception('You need to overwrite getActionName() or getSubscribedEvents() in ' . static::class);
|
||||||
|
|||||||
@@ -197,16 +197,22 @@ final class ServiceInvoice
|
|||||||
if (stripos($part, 'filename=') === false) {
|
if (stripos($part, 'filename=') === false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$filename = explode('filename=', $part);
|
$tmp = explode('filename=', $part);
|
||||||
if (\count($filename) > 1) {
|
if (\count($tmp) > 1) {
|
||||||
$filename = $filename[1];
|
$filename = $tmp[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$disposition = $event->getResponse()->headers->get('Content-Type');
|
$disposition = $event->getResponse()->headers->get('Content-Type');
|
||||||
$parts = explode(';', $disposition);
|
$parts = explode(';', $disposition);
|
||||||
$parts = explode('/', $parts[0]);
|
$parts = explode('/', $parts[0]);
|
||||||
$filename .= '.' . $parts[1];
|
if (\count($parts) > 1) {
|
||||||
|
$filename .= '.' . $parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_strlen($filename) >= 150) {
|
||||||
|
throw new \Exception(sprintf('Invoice filename "%s" is too long, max. 150 characters allowed', $filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_file($invoiceDirectory . $filename)) {
|
if (is_file($invoiceDirectory . $filename)) {
|
||||||
|
|||||||
@@ -118,6 +118,6 @@ class LdapDriver
|
|||||||
if (null === $this->logger) {
|
if (null === $this->logger) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->logger->error($message, $context);
|
$this->logger->debug($message, $context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class PageActionsEventTest extends TestCase
|
|||||||
$this->assertSame($user, $sut->getUser());
|
$this->assertSame($user, $sut->getUser());
|
||||||
$this->assertEquals([], $sut->getActions());
|
$this->assertEquals([], $sut->getActions());
|
||||||
$this->assertEquals(['actions' => [], 'view' => 'bar'], $sut->getPayload());
|
$this->assertEquals(['actions' => [], 'view' => 'bar'], $sut->getPayload());
|
||||||
|
$this->assertNull($sut->getLocale());
|
||||||
|
|
||||||
$sut = new PageActionsEvent($user, ['hello' => 'world'], 'foo', 'bar');
|
$sut = new PageActionsEvent($user, ['hello' => 'world'], 'foo', 'bar');
|
||||||
$this->assertSame($user, $sut->getUser());
|
$this->assertSame($user, $sut->getUser());
|
||||||
@@ -66,6 +67,12 @@ class PageActionsEventTest extends TestCase
|
|||||||
$this->assertEquals(['foo' => ['url' => 'bar']], $sut->getActions());
|
$this->assertEquals(['foo' => ['url' => 'bar']], $sut->getActions());
|
||||||
$sut->replaceAction('foo', ['url' => 'xyz']);
|
$sut->replaceAction('foo', ['url' => 'xyz']);
|
||||||
$this->assertEquals(['foo' => ['url' => 'xyz']], $sut->getActions());
|
$this->assertEquals(['foo' => ['url' => 'xyz']], $sut->getActions());
|
||||||
|
|
||||||
|
$sut->setLocale('de');
|
||||||
|
$this->assertEquals('de', $sut->getLocale());
|
||||||
|
|
||||||
|
$sut->setLocale(null);
|
||||||
|
$this->assertNull($sut->getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSubmenu()
|
public function testSubmenu()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
*/
|
*/
|
||||||
class ThemeEventTest extends TestCase
|
class ThemeEventTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testEmpty()
|
public function testEmpty(): void
|
||||||
{
|
{
|
||||||
$sut = new ThemeEvent();
|
$sut = new ThemeEvent();
|
||||||
$this->assertNull($sut->getUser());
|
$this->assertNull($sut->getUser());
|
||||||
@@ -26,7 +26,7 @@ class ThemeEventTest extends TestCase
|
|||||||
$this->assertEquals('', $sut->getContent());
|
$this->assertEquals('', $sut->getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDefaultValues()
|
public function testDefaultValues(): void
|
||||||
{
|
{
|
||||||
$user = new User();
|
$user = new User();
|
||||||
$user->setAlias('foo');
|
$user->setAlias('foo');
|
||||||
@@ -36,17 +36,13 @@ class ThemeEventTest extends TestCase
|
|||||||
$this->assertSame($user, $sut->getUser());
|
$this->assertSame($user, $sut->getUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetterAndSetter()
|
public function testGetterAndSetter(): void
|
||||||
{
|
{
|
||||||
$user = new User();
|
$user = new User();
|
||||||
$user->setAlias('foo');
|
$user->setAlias('foo');
|
||||||
|
|
||||||
$payload = [null, '', 'test', new \stdClass()];
|
$payload = [null, '', 'test', new \stdClass()];
|
||||||
|
|
||||||
$sut = new ThemeEvent($user);
|
|
||||||
$sut->setPayload($payload);
|
|
||||||
$this->assertEquals($payload, $sut->getPayload());
|
|
||||||
|
|
||||||
$sut = new ThemeEvent($user, $payload);
|
$sut = new ThemeEvent($user, $payload);
|
||||||
$this->assertEquals($payload, $sut->getPayload());
|
$this->assertEquals($payload, $sut->getPayload());
|
||||||
|
|
||||||
@@ -57,4 +53,19 @@ class ThemeEventTest extends TestCase
|
|||||||
$sut->addContent('<script>');
|
$sut->addContent('<script>');
|
||||||
$this->assertEquals('foo<script>', $sut->getContent());
|
$this->assertEquals('foo<script>', $sut->getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
public function testDeprecatedStuff(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setAlias('foo');
|
||||||
|
|
||||||
|
$payload = [null, '', 'test', new \stdClass()];
|
||||||
|
|
||||||
|
$sut = new ThemeEvent($user);
|
||||||
|
$sut->setPayload($payload);
|
||||||
|
$this->assertEquals($payload, $sut->getPayload());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5187,21 +5187,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Event/SystemConfigurationEventTest.php
|
path: Event/SystemConfigurationEventTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Event\\\\ThemeEventTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Event/ThemeEventTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Event\\\\ThemeEventTest\\:\\:testEmpty\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Event/ThemeEventTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Event\\\\ThemeEventTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Event/ThemeEventTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\Event\\\\ThemeJavascriptTranslationsEventTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\Tests\\\\Event\\\\ThemeJavascriptTranslationsEventTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
Reference in New Issue
Block a user