release 1.17.2 (#3135)

* allow to input password interactively in console
* added block to simplify overwriting export template parts
* prevent installation in PHP 8.1
* composer update
* phpunit version to 9
* support negative amounts in excel export
* added system configuration actions for calendar, weekly timesheet, users
* added method to render text with full markdown support
This commit is contained in:
Kevin Papst
2022-02-14 17:33:52 +01:00
committed by GitHub
parent 4cb0ac37a6
commit 83a894823f
37 changed files with 1298 additions and 892 deletions

View File

@@ -0,0 +1,40 @@
<?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\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
abstract class AbstractUserCommand extends Command
{
protected function askForPassword(InputInterface $input, OutputInterface $output): string
{
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$passwordQuestion = new Question('Please enter the password: ');
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$passwordQuestion->setValidator(function (?string $value) {
$password = trim($value);
if (empty($password)) {
throw new \Exception('The password may not be empty');
}
return $value;
});
$passwordQuestion->setMaxAttempts(3);
return $helper->ask($input, $output, $passwordQuestion);
}
}

View File

@@ -12,12 +12,11 @@ namespace App\Command;
use App\User\UserService;
use App\Utils\CommandStyle;
use App\Validator\ValidationFailedException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ChangePasswordCommand extends Command
final class ChangePasswordCommand extends AbstractUserCommand
{
private $userService;
@@ -35,7 +34,7 @@ class ChangePasswordCommand extends Command
->setDescription('Change the password of a user.')
->setDefinition([
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
new InputArgument('password', InputArgument::REQUIRED, 'The password'),
new InputArgument('password', InputArgument::OPTIONAL, 'The password'),
])
->setHelp(
<<<'EOT'
@@ -59,7 +58,12 @@ EOT
protected function execute(InputInterface $input, OutputInterface $output)
{
$username = $input->getArgument('username');
$password = $input->getArgument('password');
if (null !== $input->getArgument('password')) {
$password = $input->getArgument('password');
} else {
$password = $this->askForPassword($input, $output);
}
$user = $this->userService->findUserByUsernameOrThrowException($username);

View File

@@ -13,14 +13,11 @@ use App\Entity\User;
use App\User\UserService;
use App\Utils\CommandStyle;
use App\Validator\ValidationFailedException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
final class CreateUserCommand extends Command
final class CreateUserCommand extends AbstractUserCommand
{
private $userService;
@@ -91,31 +88,4 @@ final class CreateUserCommand extends Command
return 0;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return string
*/
protected function askForPassword(InputInterface $input, OutputInterface $output): string
{
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$passwordQuestion = new Question('Please enter the password: ');
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$passwordQuestion->setValidator(function (?string $value) {
$password = trim($value);
if (empty($password)) {
throw new \Exception('The password may not be empty');
}
return $value;
});
$passwordQuestion->setMaxAttempts(3);
return $helper->ask($input, $output, $passwordQuestion);
}
}

View File

@@ -21,10 +21,7 @@ final class ThemeConfiguration implements \ArrayAccess
$this->systemConfiguration = $systemConfiguration;
}
/**
* @return bool
*/
public function offsetExists($offset)
public function offsetExists($offset): bool
{
return $this->systemConfiguration->has('theme.' . $offset);
}
@@ -42,7 +39,7 @@ final class ThemeConfiguration implements \ArrayAccess
* @param mixed $value
* @throws \BadMethodCallException
*/
public function offsetSet($offset, $value)
public function offsetSet($offset, $value): void
{
throw new \BadMethodCallException('ThemeConfiguration does not support offsetSet()');
}
@@ -51,7 +48,7 @@ final class ThemeConfiguration implements \ArrayAccess
* @param mixed $offset
* @throws \BadMethodCallException
*/
public function offsetUnset($offset)
public function offsetUnset($offset): void
{
throw new \BadMethodCallException('ThemeConfiguration does not support offsetUnset()');
}

View File

@@ -24,6 +24,10 @@ class CalendarSubscriber extends AbstractActionsSubscriber
$event->addCreate($this->path('timesheet_create'));
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'calendar']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('calendar.html'));
}
}

View File

@@ -20,6 +20,10 @@ class QuickEntrySubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'quick_entry']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('weekly-times.html'));
}
}

View File

@@ -38,6 +38,10 @@ class UsersSubscriber extends AbstractActionsSubscriber
$event->addCreate($this->path('admin_user_create'), false);
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'user']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('users.html'));
}
}

View File

@@ -45,8 +45,10 @@ abstract class AbstractSpreadsheetRenderer
public const TIME_FORMAT = 'hh:mm';
public const DURATION_FORMAT = '[hh]:mm';
public const DURATION_DECIMAL = '#0.00';
// https://support.microsoft.com/de-de/office/zahlenformatcodes-5026bbd6-04bc-48cd-bf33-80f18b4eae68
// Part 1 = positive; Part 2 = negative; Part 3 = zero; Part 4 = Text
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* \(#,##0.00\);_("%1$s"* "-"??_);_(@_)';
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* -#,##0.00;_("%1$s"* "-"??_);_(@_)';
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
protected $durationFormat = self::DURATION_FORMAT;

View File

@@ -49,11 +49,11 @@ class PDFRenderer
*/
private $pdfOptions = [];
public function __construct(Environment $twig, HtmlToPdfConverter $converter, ProjectStatisticService $projectRepository)
public function __construct(Environment $twig, HtmlToPdfConverter $converter, ProjectStatisticService $projectStatisticService)
{
$this->twig = $twig;
$this->converter = $converter;
$this->projectStatisticService = $projectRepository;
$this->projectStatisticService = $projectStatisticService;
}
protected function getTemplate(): string

View File

@@ -53,7 +53,6 @@ class TimesheetEditForm extends AbstractType
$project = null;
$customer = null;
$currency = false;
$begin = null;
$customerCount = $this->customers->countCustomer(true);
$timezone = $options['timezone'];
$isNew = true;

View File

@@ -126,6 +126,6 @@ final class MarkdownExtension implements RuntimeExtensionInterface
*/
public function markdownToHtml(string $content): string
{
return $this->markdown->toHtml($content);
return $this->markdown->withFullMarkdownSupport($content);
}
}

View File

@@ -33,14 +33,14 @@ class Duration
* @param string $format
* @return string|null
*/
public function format($seconds, $format = self::FORMAT_NO_SECONDS)
public function format(?int $seconds, string $format = self::FORMAT_NO_SECONDS)
{
if (null === $seconds) {
return null;
}
$hour = floor($seconds / 3600);
$minute = floor(($seconds / 60) % 60);
$hour = (int) floor($seconds / 3600);
$minute = (int) floor((int) ($seconds / 60) % 60);
$hour = $hour > 9 ? $hour : '0' . $hour;
$minute = $minute > 9 ? $minute : '0' . $minute;
@@ -60,7 +60,7 @@ class Duration
* @param string $duration
* @return int
*/
public function parseDurationString($duration): int
public function parseDurationString(string $duration): int
{
if (false !== stripos($duration, ':')) {
return $this->parseDuration($duration, self::FORMAT_COLON);

View File

@@ -83,7 +83,6 @@ class MPdfConverter implements HtmlToPdfConverter
for ($i = 0; $i < \count($parts); $i++) {
if (stripos($parts[$i], '<!-- CONTENT_PART -->') !== false) {
$subParts = explode('<!-- CONTENT_PART -->', $parts[$i]);
$run = 0;
foreach ($subParts as $subPart) {
$mpdf->WriteHTML($subPart);
}

View File

@@ -10,7 +10,7 @@
namespace App\Utils;
/**
* A simple class to parse markdown syntax and return HTML.
* Parse markdown syntax and return HTML.
*/
final class Markdown
{
@@ -19,20 +19,14 @@ final class Markdown
*/
private $parser;
public function __construct()
{
$this->parser = new ParsedownExtension();
$this->parser->setUrlsLinked(true);
$this->parser->setBreaksEnabled(true);
}
/**
* @param string $text
* @param bool $safe
* @return string
*/
public function toHtml(string $text, bool $safe = true): string
{
if ($this->parser === null) {
$this->parser = new ParsedownExtension();
$this->parser->setUrlsLinked(true);
$this->parser->setBreaksEnabled(true);
}
if ($safe !== true) {
@trigger_error('Only safe mode is supported in Markdown since 1.16.3 to prevent XSS attacks. Parameter $safe will be removed with 2.0', E_USER_DEPRECATED);
}
@@ -42,4 +36,15 @@ final class Markdown
return $this->parser->text($text);
}
public function withFullMarkdownSupport(string $text): string
{
$parser = new \Parsedown();
$parser->setUrlsLinked(true);
$parser->setBreaksEnabled(true);
$parser->setSafeMode(true);
$parser->setMarkupEscaped(true);
return $parser->text($text);
}
}

View File

@@ -24,7 +24,7 @@ class ParsedownExtension extends \Parsedown
protected $BlockTypes = [
'*' => ['Rule', 'List'],
'+' => ['List'],
'-' => ['SetextHeader', 'Table', 'Rule', 'List'],
'-' => ['Table', 'Rule', 'List'],
'0' => ['List'],
'1' => ['List'],
'2' => ['List'],
@@ -37,7 +37,6 @@ class ParsedownExtension extends \Parsedown
'9' => ['List'],
':' => ['Table'],
'<' => ['Comment', 'Markup'],
'=' => ['SetextHeader'],
'>' => ['Quote'],
'[' => ['Reference'],
'_' => ['Rule'],
@@ -124,7 +123,7 @@ class ParsedownExtension extends \Parsedown
* @param string $text
* @return string
*/
private function getIDfromText($text)
private function getIDfromText($text): string
{
$text = strtolower($text);