Release 2.2.0 (#4359)

* deactivate deprecation logging in prod for now
* fix several deprecations
* enable CSRF for logout
* allow more twig methods and functions in InvoiceSecurity policy
This commit is contained in:
Kevin Papst
2023-10-31 16:41:09 +01:00
committed by GitHub
parent 114617a052
commit 95f15e6c88
22 changed files with 396 additions and 324 deletions

368
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
when@prod:
monolog:
channels: ["deprecation"]
# channels: ["deprecation"]
handlers:
main:
type: fingers_crossed
@@ -15,10 +15,11 @@ when@prod:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: ["deprecation"]
path: "%kernel.logs_dir%/deprecations.log"
# deactivated, because currently there are too many deprecations cause by gedmo and doctrine
# deprecation:
# type: stream
# channels: ["deprecation"]
# path: "%kernel.logs_dir%/deprecations.log"
when@dev:
monolog:

View File

@@ -60,6 +60,7 @@ security:
logout:
path: logout
target: homepage
enable_csrf: true
login_throttling:
max_attempts: 5

View File

@@ -77,7 +77,7 @@ final class UserLoginLinkCommand extends Command
$loginLink = $loginLinkDetails->getUrl();
if ($input->getOption('password-reset') === true) {
$user->setPasswordRequestedAt(new \DateTime());
$user->markPasswordRequested();
$user->setRequiresPasswordReset(true);
$this->userRepository->saveUser($user);
}

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.1.0';
public const VERSION = '2.2.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20100;
public const VERSION_ID = 20200;
/**
* The software name
*/

View File

@@ -17,7 +17,6 @@ use App\Event\EmailPasswordResetEvent;
use App\Form\PasswordResetForm;
use App\User\LoginManager;
use App\User\UserService;
use DateTime;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Form\FormInterface;
@@ -83,7 +82,7 @@ final class PasswordResetController extends AbstractController
// this will finally send the email
$this->eventDispatcher->dispatch(new EmailEvent($event->getEmail()));
$user->setPasswordRequestedAt(new DateTime());
$user->markPasswordRequested();
$this->userService->updateUser($user);
}
@@ -138,8 +137,7 @@ final class PasswordResetController extends AbstractController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user->setConfirmationToken(null);
$user->setPasswordRequestedAt(null);
$user->markPasswordResetted();
$user->setEnabled(true);
$this->userService->updateUser($user);

View File

@@ -188,8 +188,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
#[Assert\NotNull]
private ?string $category = self::WORK;
#[ORM\Column(name: 'modified_at', type: 'datetime', nullable: true)]
private \DateTimeInterface $modifiedAt;
#[ORM\Column(name: 'modified_at', type: 'datetime_immutable', nullable: true)]
private \DateTimeImmutable $modifiedAt; // @phpstan-ignore-line - create migration and update all null values and then make it not null
/**
* Tags
*
@@ -586,12 +586,12 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
return $this;
}
public function getModifiedAt(): \DateTimeInterface
public function getModifiedAt(): \DateTimeImmutable
{
return $this->modifiedAt;
}
public function setModifiedAt(\DateTimeInterface $dateTime): void
public function setModifiedAt(\DateTimeImmutable $dateTime): void
{
$this->modifiedAt = $dateTime;
}

View File

@@ -196,8 +196,8 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
#[ORM\Column(name: 'confirmation_token', type: 'string', length: 180, unique: true, nullable: true)]
#[Assert\Length(max: 180)]
private ?string $confirmationToken = null;
#[ORM\Column(name: 'password_requested_at', type: 'datetime', nullable: true)]
private ?\DateTime $passwordRequestedAt = null;
#[ORM\Column(name: 'password_requested_at', type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $passwordRequestedAt = null;
/**
* List of all role names
*/
@@ -958,26 +958,31 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
return $this;
}
public function setConfirmationToken($confirmationToken): User
public function setConfirmationToken($confirmationToken): void
{
$this->confirmationToken = $confirmationToken;
return $this;
}
public function setPasswordRequestedAt(?\DateTime $date = null): User
public function markPasswordRequested(): void
{
$this->setPasswordRequestedAt(new \DateTimeImmutable('now', new \DateTimeZone($this->getTimezone())));
}
public function markPasswordResetted(): void
{
$this->setConfirmationToken(null);
$this->setPasswordRequestedAt(null);
}
public function setPasswordRequestedAt(?\DateTimeImmutable $date): void
{
$this->passwordRequestedAt = $date;
return $this;
}
/**
* Gets the timestamp that the user requested a password reset.
*
* @return DateTime|null
*/
public function getPasswordRequestedAt(): ?DateTime
public function getPasswordRequestedAt(): ?\DateTimeImmutable
{
return $this->passwordRequestedAt;
}

View File

@@ -41,9 +41,9 @@ class WorkingTime
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')]
private ?User $approvedBy = null;
#[ORM\Column(name: 'approved_at', type: 'datetime', nullable: true)]
#[ORM\Column(name: 'approved_at', type: 'datetime_immutable', nullable: true)]
#[Assert\NotNull]
private ?\DateTimeInterface $approvedAt = null;
private ?\DateTimeImmutable $approvedAt = null;
public function __construct(User $user, \DateTimeInterface $date)
{
@@ -96,12 +96,12 @@ class WorkingTime
$this->approvedBy = $approvedBy;
}
public function getApprovedAt(): ?\DateTimeInterface
public function getApprovedAt(): ?\DateTimeImmutable
{
return $this->approvedAt;
}
public function setApprovedAt(?\DateTimeInterface $approvedAt): void
public function setApprovedAt(?\DateTimeImmutable $approvedAt): void
{
$this->approvedAt = $approvedAt;
}

View File

@@ -9,15 +9,15 @@
namespace App\Model;
use DateTimeInterface;
use DateTimeImmutable;
class Day
{
public function __construct(private DateTimeInterface $day)
public function __construct(private DateTimeImmutable $day)
{
}
public function getDay(): DateTimeInterface
public function getDay(): DateTimeImmutable
{
return $this->day;
}

View File

@@ -20,7 +20,7 @@ class Month
public function __construct(private \DateTimeInterface $month)
{
$date = new \DateTimeImmutable($this->month->format('Y-m-01 00:00:00'));
$date = new \DateTimeImmutable($this->month->format('Y-m-01 00:00:00'), $month->getTimezone());
$start = $date->format('m');
while ($start === $date->format('m')) {
$day = $this->createDay($date);
@@ -29,7 +29,7 @@ class Month
}
}
protected function createDay(\DateTimeInterface $day): Day
protected function createDay(\DateTimeImmutable $day): Day
{
return new Day($day);
}

View File

@@ -20,17 +20,15 @@ class Year
public function __construct(private DateTimeInterface $month)
{
$monthDate = new \DateTimeImmutable();
$monthDate = $monthDate->setDate((int) $this->month->format('Y'), 1, 1);
$monthDate = $monthDate->setTime(1, 0);
$monthDate = new \DateTimeImmutable($this->month->format('Y-01-01 01:00:00'), $this->month->getTimezone());
for ($i = 1; $i < 13; $i++) {
$month = $this->createMonth($monthDate);
$this->setMonth($month);
$tmp = $this->createMonth($monthDate);
$this->setMonth($tmp);
$monthDate = $monthDate->add(new \DateInterval('P1M'));
}
}
protected function createMonth(\DateTimeInterface $month): Month
protected function createMonth(\DateTimeImmutable $month): Month
{
return new Month($month);
}

View File

@@ -31,14 +31,113 @@ final class InvoicePolicy implements SecurityPolicyInterface
$this->policy->addPolicy(new SecurityPolicy(
['block', 'if', 'for', 'set', 'extends'],
[
// Twig core filters
'map', 'escape', 'trans', 'default', 'nl2br', 'trim', 'raw',
'join', 'u', 'slice', 'date', 'month_name', 'first', 'country_name',
'replace', 'length', 'number_format', 'split',
// =================================================================
// vendor/twig/twig/src/Extension/CoreExtension.php
// Kimai filters
'md2html', 'desc2html', 'comment2html', 'comment1line', 'multiline_indent', 'nl2str',
'date_short', 'duration', 'amount', 'money', 'duration_decimal',
// formatting filters
'date',
'date_modify',
'format',
'replace',
'number_format',
'abs',
'round',
// encoding
'url_encode',
'json_encode',
'convert_encoding',
// string filters
'title',
'capitalize',
'upper',
'lower',
'striptags',
'trim',
'nl2br',
'spaceless',
// array helpers
'join',
'split',
'sort',
'merge',
'batch',
'column',
'filter',
'map',
'reduce',
// string/array filters
'reverse',
'length',
'slice',
'first',
'last',
// iteration and runtime
'default',
'keys',
// =================================================================
// vendor/twig/twig/src/Extension/EscaperExtension.php
'escape',
'e',
'raw',
// =================================================================
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
'trans',
// =================================================================
// vendor/twig/string-extra/StringExtension.php
'u',
'slug',
// =================================================================
// vendor/twig/intl-extra/IntlExtension.php
'country_name',
'currency_name',
'currency_symbol',
'language_name',
'locale_name',
'format_currency',
'format_number',
'format_*_number',
'format_datetime',
'format_date',
'format_time',
// =================================================================
// src/Twig/LocaleFormatExtensions.php
'month_name',
'day_name',
'date_short',
'date_time',
'date_full',
'date_format',
'date_weekday',
'time',
'duration',
'duration_decimal',
'money',
'amount',
// =================================================================
// src/Twig/RuntimeExtensions.php
'md2html',
'desc2html',
'comment2html',
'comment1line',
// =================================================================
// src/Twig/Extensions.php
'multiline_indent',
'color',
'font_contrast',
'default_color',
'nl2str',
],
[
PdfContext::class => ['setoption'],
@@ -46,11 +145,37 @@ final class InvoicePolicy implements SecurityPolicyInterface
],
[], // properties
[
// Twig core functions
'cycle', 'asset', 'range',
// =================================================================
// vendor/twig/twig/src/Extension/CoreExtension.php
'max',
'min',
'range',
'constant',
'cycle',
'random',
'date',
'asset',
'range',
// Kimai functions
'encore_entry_css_source', 'qr_code_data_uri', 'config',
// =================================================================
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
't',
// =================================================================
// vendor/symfony/webpack-encore-bundle/src/Twig/EntryFilesTwigExtension.php
'encore_entry_css_source',
// =================================================================
// vendor/symfony/twig-bridge/Extension/AssetExtension.php
'asset',
// =================================================================
// Twig/RuntimeExtensions.php
'qr_code_data_uri',
// =================================================================
// Twig/Configuration.php
'config',
]
));
}

View File

@@ -18,7 +18,7 @@ use App\Model\Month as BaseMonth;
*/
final class Month extends BaseMonth
{
public function __construct(\DateTimeInterface $month, private User $user)
public function __construct(\DateTimeImmutable $month, private User $user)
{
parent::__construct($month);
}
@@ -67,7 +67,7 @@ final class Month extends BaseMonth
return null;
}
protected function createDay(\DateTimeInterface $day): Day
protected function createDay(\DateTimeImmutable $day): Day
{
return new Day($day);
}

View File

@@ -28,7 +28,7 @@ final class Year extends BaseYear
return $this->user;
}
protected function createMonth(\DateTimeInterface $month): Month
protected function createMonth(\DateTimeImmutable $month): Month
{
return new Month($month, $this->user);
}

View File

@@ -117,7 +117,8 @@ final class WorkingTimeService
}
$workingTime->setApprovedBy($approver);
$workingTime->setApprovedAt($approvalDate);
// FIXME see calling method
$workingTime->setApprovedAt(\DateTimeImmutable::createFromInterface($approvalDate));
$this->workingTimeRepository->scheduleWorkingTimeUpdate($workingTime);
}

View File

@@ -24,14 +24,17 @@ class SystemConfigurationTest extends TestCase
* @param array $loaderSettings
* @return SystemConfiguration
*/
protected function getSut(array $settings, array $loaderSettings = [])
protected function getSut(array $settings, array $loaderSettings = []): SystemConfiguration
{
$loader = new TestConfigLoader($loaderSettings);
return SystemConfigurationFactory::create($loader, $settings);
}
protected function getDefaultSettings()
/**
* @return array<string, array<mixed>>
*/
protected function getDefaultSettings(): array
{
return [
'timesheet' => [
@@ -106,7 +109,10 @@ class SystemConfigurationTest extends TestCase
];
}
protected function getDefaultLoaderSettings()
/**
* @return array<Configuration>
*/
protected function getDefaultLoaderSettings(): array
{
return [
(new Configuration())->setName('defaults.customer.timezone')->setValue('Russia/Moscov'),
@@ -123,7 +129,7 @@ class SystemConfigurationTest extends TestCase
];
}
public function testDefaultWithoutLoader()
public function testDefaultWithoutLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertEquals('Europe/London', $sut->find('defaults.customer.timezone'));
@@ -133,7 +139,7 @@ class SystemConfigurationTest extends TestCase
$this->assertEquals('Maroon|#800000,Brown|#a52a2a,Red|#ff0000,Orange|#ffa500,#ffffff,,|#000000', $sut->getThemeColorChoices());
}
public function testDefaultWithLoader()
public function testDefaultWithLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
$this->assertEquals('Russia/Moscov', $sut->find('defaults.customer.timezone'));
@@ -143,7 +149,7 @@ class SystemConfigurationTest extends TestCase
$this->assertFalse($sut->isSamlActive());
}
public function testDefaultWithMixedConfigs()
public function testDefaultWithMixedConfigs(): void
{
$sut = $this->getSut($this->getDefaultSettings(), [
(new Configuration())->setName('timesheet.rules.allow_future_times')->setValue(''),
@@ -157,7 +163,7 @@ class SystemConfigurationTest extends TestCase
$this->assertEquals('2020-03-27', $sut->getFinancialYearStart());
}
public function testOffsetUnsetThrowsException()
public function testOffsetUnsetThrowsException(): void
{
$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('SystemBundleConfiguration does not support offsetUnset()');
@@ -166,7 +172,7 @@ class SystemConfigurationTest extends TestCase
$sut->offsetUnset('dfsdf');
}
public function testUnknownConfigs()
public function testUnknownConfigs(): void
{
$sut = $this->getSut($this->getDefaultSettings(), [
(new Configuration())->setName('timesheet.foo')->setValue('hello'),
@@ -182,7 +188,7 @@ class SystemConfigurationTest extends TestCase
$this->assertEquals('foooo-bar!', $sut->find('xxxxxxxx.yyyyyyyyy'));
}
public function testCalendarWithoutLoader()
public function testCalendarWithoutLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertEquals('07:49', $sut->getCalendarBusinessTimeBegin());
@@ -199,7 +205,7 @@ class SystemConfigurationTest extends TestCase
$this->assertEquals(2, \count($sources));
}
public function testCalendarWithLoader()
public function testCalendarWithLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
$this->assertEquals('00:30:00', $sut->getCalendarSlotDuration());
@@ -207,7 +213,7 @@ class SystemConfigurationTest extends TestCase
$this->assertEquals(2, \count($sources));
}
public function testFormDefaultWithoutLoader()
public function testFormDefaultWithoutLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertEquals('Europe/London', $sut->getCustomerDefaultTimezone());
@@ -220,7 +226,7 @@ class SystemConfigurationTest extends TestCase
$this->assertNull($sut->getFinancialYearStart());
}
public function testFormDefaultWithLoader()
public function testFormDefaultWithLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
$this->assertEquals('Russia/Moscov', $sut->getCustomerDefaultTimezone());
@@ -232,7 +238,7 @@ class SystemConfigurationTest extends TestCase
$this->assertEquals('USD', $sut->getUserDefaultCurrency());
}
public function testTimesheetWithoutLoader()
public function testTimesheetWithoutLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertEquals(99, $sut->getTimesheetActiveEntriesHardLimit());
@@ -250,7 +256,7 @@ class SystemConfigurationTest extends TestCase
$this->assertEquals(5, $sut->getTimesheetIncrementMinutes());
}
public function testTimesheetWithLoader()
public function testTimesheetWithLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
$this->assertEquals(7, $sut->getTimesheetActiveEntriesHardLimit());

View File

@@ -38,7 +38,7 @@ class LayoutControllerTest extends ControllerBaseTest
$this->assertStringContainsString('href="/en/profile/' . $user->getUserIdentifier() . '"', $content);
$this->assertStringContainsString('href="/en/profile/' . $user->getUserIdentifier() . '/edit"', $content);
$this->assertStringContainsString('href="/en/profile/' . $user->getUserIdentifier() . '/prefs"', $content);
$this->assertStringContainsString('href="/en/logout"', $content);
$this->assertStringContainsString('href="/en/logout?_csrf_token=', $content);
}
protected function assertHasNavigation(HttpKernelBrowser $client)

View File

@@ -183,7 +183,7 @@ class TimesheetTest extends TestCase
$sut->setExported(true);
$sut->setDescription('Invalid timesheet category "foo" given, expected one of: work, holiday, sickness, parental, overtime');
$modifiedDate = new \DateTime();
$modifiedDate = new \DateTimeImmutable();
$reflection = new \ReflectionClass($sut);
$property = $reflection->getProperty('modifiedAt');

View File

@@ -219,7 +219,7 @@ class UserTest extends TestCase
public function testPasswordRequestedAt(): void
{
$date = new \DateTime('-60 minutes');
$date = new \DateTimeImmutable('-60 minutes');
$sut = new User();
self::assertFalse($sut->isPasswordRequestNonExpired(3599));

View File

@@ -22,7 +22,7 @@ class WorkingTimeTest extends TestCase
{
$user = new User();
$user->setUsername('bar');
$date = new \DateTime();
$date = new \DateTimeImmutable();
$sut = new WorkingTime($user, $date);
self::assertSame($user, $sut->getUser());
@@ -36,7 +36,7 @@ class WorkingTimeTest extends TestCase
self::assertNull($sut->getApprovedBy());
self::assertFalse($sut->isApproved());
$approvedAt = new \DateTime('2023-01-01 12:00:00', new \DateTimeZone('Europe/Vienna'));
$approvedAt = new \DateTimeImmutable('2023-01-01 12:00:00', new \DateTimeZone('Europe/Vienna'));
$approvedBy = new User();
$approvedBy->setUsername('foo');

View File

@@ -1922,16 +1922,6 @@ parameters:
count: 1
path: Configuration/SamlConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:getDefaultLoaderSettings\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:getDefaultSettings\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:getSut\\(\\) has parameter \\$loaderSettings with no value type specified in iterable type array\\.$#"
count: 1
@@ -1942,61 +1932,6 @@ parameters:
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testCalendarWithLoader\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testCalendarWithoutLoader\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testDefaultWithLoader\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testDefaultWithMixedConfigs\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testDefaultWithoutLoader\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testFormDefaultWithLoader\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testFormDefaultWithoutLoader\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testOffsetUnsetThrowsException\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testTimesheetWithLoader\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testTimesheetWithoutLoader\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testUnknownConfigs\\(\\) has no return type specified\\.$#"
count: 1
path: Configuration/SystemConfigurationTest.php
-
message: "#^Method App\\\\Tests\\\\ConsoleApplicationTest\\:\\:testVersion\\(\\) has no return type specified\\.$#"
count: 1