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:
|
||||
|
||||
- [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) — stable and "almost released" (waiting for some major plugins, which are not yet migrated)
|
||||
- [Version 2](https://github.com/kimai/kimai) — the current stable release (PHP 8.1 only)
|
||||
- [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
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
|
||||
|
||||
25
phpstan.neon
25
phpstan.neon
@@ -2266,11 +2266,6 @@ parameters:
|
||||
count: 9
|
||||
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\\.$#"
|
||||
count: 1
|
||||
@@ -4531,11 +4526,6 @@ parameters:
|
||||
count: 1
|
||||
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\\.$#"
|
||||
count: 1
|
||||
@@ -4581,11 +4571,6 @@ parameters:
|
||||
count: 1
|
||||
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\\.$#"
|
||||
count: 1
|
||||
@@ -4601,11 +4586,6 @@ parameters:
|
||||
count: 1
|
||||
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\\.$#"
|
||||
count: 2
|
||||
@@ -4621,11 +4601,6 @@ parameters:
|
||||
count: 1
|
||||
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\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -46,6 +46,7 @@ final class ActionsController extends BaseApiController
|
||||
*/
|
||||
private function convertEvent(PageActionsEvent $event, string $locale): array
|
||||
{
|
||||
$event->setLocale($locale);
|
||||
$this->dispatcher->dispatch($event, $event->getEventName());
|
||||
|
||||
$translator = $this->translator;
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0.18';
|
||||
public const VERSION = '2.0.19';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20018;
|
||||
public const VERSION_ID = 20019;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -18,9 +18,10 @@ use App\Entity\User;
|
||||
*/
|
||||
class PageActionsEvent extends ThemeEvent
|
||||
{
|
||||
private $action;
|
||||
private $view;
|
||||
private $divider = 0;
|
||||
private string $action;
|
||||
private string $view;
|
||||
private int $divider = 0;
|
||||
private ?string $locale = null;
|
||||
|
||||
public function __construct(User $user, array $payload, string $action, string $view)
|
||||
{
|
||||
@@ -195,4 +196,14 @@ class PageActionsEvent extends ThemeEvent
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.0.19, will be removed with 2.1
|
||||
*/
|
||||
public function setPayload(mixed $payload): void
|
||||
{
|
||||
@trigger_error('ThemeEvent::setPayload() is deprecated, use AbstractActionsSubscriber instead.', E_USER_DEPRECATED);
|
||||
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
*/
|
||||
abstract class AbstractActionsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private ?string $locale = null;
|
||||
|
||||
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
|
||||
{
|
||||
if ($this->locale !== null) {
|
||||
$parameters['_locale'] = $this->locale;
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate($route, $parameters);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
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
|
||||
{
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
$filename = explode('filename=', $part);
|
||||
if (\count($filename) > 1) {
|
||||
$filename = $filename[1];
|
||||
$tmp = explode('filename=', $part);
|
||||
if (\count($tmp) > 1) {
|
||||
$filename = $tmp[1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$disposition = $event->getResponse()->headers->get('Content-Type');
|
||||
$parts = explode(';', $disposition);
|
||||
$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)) {
|
||||
|
||||
@@ -118,6 +118,6 @@ class LdapDriver
|
||||
if (null === $this->logger) {
|
||||
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->assertEquals([], $sut->getActions());
|
||||
$this->assertEquals(['actions' => [], 'view' => 'bar'], $sut->getPayload());
|
||||
$this->assertNull($sut->getLocale());
|
||||
|
||||
$sut = new PageActionsEvent($user, ['hello' => 'world'], 'foo', 'bar');
|
||||
$this->assertSame($user, $sut->getUser());
|
||||
@@ -66,6 +67,12 @@ class PageActionsEventTest extends TestCase
|
||||
$this->assertEquals(['foo' => ['url' => 'bar']], $sut->getActions());
|
||||
$sut->replaceAction('foo', ['url' => 'xyz']);
|
||||
$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()
|
||||
|
||||
@@ -18,7 +18,7 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
class ThemeEventTest extends TestCase
|
||||
{
|
||||
public function testEmpty()
|
||||
public function testEmpty(): void
|
||||
{
|
||||
$sut = new ThemeEvent();
|
||||
$this->assertNull($sut->getUser());
|
||||
@@ -26,7 +26,7 @@ class ThemeEventTest extends TestCase
|
||||
$this->assertEquals('', $sut->getContent());
|
||||
}
|
||||
|
||||
public function testDefaultValues()
|
||||
public function testDefaultValues(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
@@ -36,17 +36,13 @@ class ThemeEventTest extends TestCase
|
||||
$this->assertSame($user, $sut->getUser());
|
||||
}
|
||||
|
||||
public function testGetterAndSetter()
|
||||
public function testGetterAndSetter(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$payload = [null, '', 'test', new \stdClass()];
|
||||
|
||||
$sut = new ThemeEvent($user);
|
||||
$sut->setPayload($payload);
|
||||
$this->assertEquals($payload, $sut->getPayload());
|
||||
|
||||
$sut = new ThemeEvent($user, $payload);
|
||||
$this->assertEquals($payload, $sut->getPayload());
|
||||
|
||||
@@ -57,4 +53,19 @@ class ThemeEventTest extends TestCase
|
||||
$sut->addContent('<script>');
|
||||
$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
|
||||
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\\.$#"
|
||||
count: 1
|
||||
|
||||
Reference in New Issue
Block a user