Release 2.48 (#5789)
This commit is contained in:
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.47.0';
|
||||
public const VERSION = '2.48.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 24700;
|
||||
public const VERSION_ID = 24800;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -108,10 +108,30 @@ final class DoctorController extends AbstractController
|
||||
'logLines' => $logLines,
|
||||
'logSize' => $this->getLogSize(),
|
||||
'composer' => $this->getComposerPackages(),
|
||||
'release' => $latestRelease
|
||||
'release' => $latestRelease,
|
||||
'opcache' => $this->getOpcacheConfiguration()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled: bool, status: false|array<mixed>}
|
||||
*/
|
||||
private function getOpcacheConfiguration(): array
|
||||
{
|
||||
$status = \function_exists('opcache_get_status') ? opcache_get_status() : false;
|
||||
|
||||
$enabled = \is_array($status) && $status['opcache_enabled'];
|
||||
|
||||
if ($enabled && \array_key_exists('scripts', $status)) {
|
||||
unset($status['scripts']);
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'status' => $status,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
@@ -264,7 +284,12 @@ final class DoctorController extends AbstractController
|
||||
'sys_temp_dir',
|
||||
'date.timezone',
|
||||
'session.gc_maxlifetime',
|
||||
'disable_functions'
|
||||
'disable_functions',
|
||||
'opcache.enable',
|
||||
'opcache.memory_consumption',
|
||||
'opcache.interned_strings_buffer',
|
||||
'opcache.max_accelerated_files',
|
||||
'opcache.validate_timestamps',
|
||||
];
|
||||
|
||||
$settings = [];
|
||||
|
||||
@@ -256,6 +256,18 @@ class ExportTemplate
|
||||
return \is_string($font) ? $font : null;
|
||||
}
|
||||
|
||||
public function isAvailableForAll(): bool
|
||||
{
|
||||
$isAllowed = $this->getOption('user_access', false);
|
||||
|
||||
return \is_bool($isAllowed) ? $isAllowed : false;
|
||||
}
|
||||
|
||||
public function setAvailableForAll(bool $userAccess): void
|
||||
{
|
||||
$this->setOption('user_access', $userAccess);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->title ?? 'New';
|
||||
|
||||
@@ -34,7 +34,7 @@ final class TimesheetsSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('export_own_timesheet')) {
|
||||
foreach ($this->serviceExport->getTimesheetExporter() as $exporter) {
|
||||
$event->addActionToSubmenu('export', $exporter->getId(), ['url' => $this->path('timesheet_export', ['exporter' => $exporter->getId()]), 'class' => 'toolbar-action', 'title' => 'button.' . $exporter->getId(), 'translation_domain' => 'messages']);
|
||||
$event->addActionToSubmenu('export', $exporter->getId(), ['url' => $this->path('timesheet_export', ['exporter' => $exporter->getId()]), 'class' => 'toolbar-action', 'title' => $exporter->getTitle()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class TimesheetsTeamSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('export_other_timesheet')) {
|
||||
foreach ($this->serviceExport->getTimesheetExporter() as $exporter) {
|
||||
$event->addActionToSubmenu('export', $exporter->getId(), ['url' => $this->path('admin_timesheet_export', ['exporter' => $exporter->getId()]), 'class' => 'toolbar-action', 'title' => 'button.' . $exporter->getId(), 'translation_domain' => 'messages']);
|
||||
$event->addActionToSubmenu('export', $exporter->getId(), ['url' => $this->path('admin_timesheet_export', ['exporter' => $exporter->getId()]), 'class' => 'toolbar-action', 'title' => $exporter->getTitle()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class CsvRendererFactory
|
||||
|
||||
public function createDefault(): CsvRenderer
|
||||
{
|
||||
$template = new DefaultTemplate($this->eventDispatcher, 'csv');
|
||||
$template = new DefaultTemplate($this->eventDispatcher, 'csv', 'en', 'csv');
|
||||
|
||||
return new CsvRenderer($this->converter, $this->translator, $template);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ final class HtmlRendererFactory
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(string $id, string $template): HtmlRenderer
|
||||
public function create(string $id, string $template, string $title = 'print'): HtmlRenderer
|
||||
{
|
||||
return new HtmlRenderer(
|
||||
$this->twig,
|
||||
@@ -33,7 +33,7 @@ final class HtmlRendererFactory
|
||||
$this->projectStatisticService,
|
||||
$this->activityStatisticService,
|
||||
$id,
|
||||
'print',
|
||||
$title,
|
||||
$template
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ final class XlsxRendererFactory
|
||||
|
||||
public function createDefault(): XlsxRenderer
|
||||
{
|
||||
$template = new DefaultTemplate($this->eventDispatcher, 'xlsx');
|
||||
$template = new DefaultTemplate($this->eventDispatcher, 'xlsx', 'en', 'xlsx');
|
||||
|
||||
return new XlsxRenderer($this->converter, $this->translator, $template);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Export;
|
||||
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Entity\ExportTemplate;
|
||||
use App\Event\ExportItemsQueryEvent;
|
||||
use App\Export\Renderer\CsvRendererFactory;
|
||||
use App\Export\Renderer\HtmlRendererFactory;
|
||||
@@ -74,6 +75,15 @@ final class ServiceExport
|
||||
$this->renderer[] = $renderer;
|
||||
}
|
||||
|
||||
private function filenameToTitle(string $title): string
|
||||
{
|
||||
if (str_contains($title, '.')) {
|
||||
$title = explode('.', $title)[0];
|
||||
}
|
||||
|
||||
return str_replace(['-', '_'], ' ', $title);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExportRendererInterface[]
|
||||
*/
|
||||
@@ -82,32 +92,15 @@ final class ServiceExport
|
||||
$renderer = [
|
||||
$this->csvRendererFactory->createDefault(),
|
||||
$this->xlsxRendererFactory->createDefault(),
|
||||
$this->pdfRendererFactory->create('pdf', 'export/pdf-layout.html.twig', 'default'),
|
||||
$this->htmlRendererFactory->create('html', 'export/print.html.twig'),
|
||||
$this->pdfRendererFactory->create('pdf', 'export/pdf-layout.html.twig', 'pdf'),
|
||||
$this->htmlRendererFactory->create('print', 'export/print.html.twig'),
|
||||
];
|
||||
|
||||
foreach ($this->exportTemplateRepository->findAll() as $template) {
|
||||
$tpl = new Template((string) $template->getId(), $template->getTitle()); // @phpstan-ignore argument.type
|
||||
$tpl->setColumns($template->getColumns());
|
||||
$tpl->setLocale($template->getLanguage());
|
||||
$tpl->setOptions($template->getOptions());
|
||||
|
||||
switch ($template->getRenderer()) {
|
||||
case 'csv':
|
||||
$renderer[] = $this->csvRendererFactory->create($tpl);
|
||||
break;
|
||||
|
||||
case 'xlsx':
|
||||
$renderer[] = $this->xlsxRendererFactory->create($tpl);
|
||||
break;
|
||||
|
||||
case 'pdf':
|
||||
$renderer[] = $this->pdfRendererFactory->createFromTemplate($tpl);
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->logger->error('Unknown export template type: ' . $template->getRenderer());
|
||||
break;
|
||||
try {
|
||||
$renderer[] = $this->createTemplateFromExportTemplate($template);
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->error('Unknown export template type: ' . $template->getRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +117,7 @@ final class ServiceExport
|
||||
continue;
|
||||
}
|
||||
|
||||
$renderer[] = $this->htmlRendererFactory->create($tplName, '@export/' . $tplName);
|
||||
$renderer[] = $this->htmlRendererFactory->create($tplName, '@export/' . $tplName, $this->filenameToTitle($tplName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +129,7 @@ final class ServiceExport
|
||||
continue;
|
||||
}
|
||||
|
||||
$renderer[] = $this->pdfRendererFactory->create($tplName, '@export/' . $tplName);
|
||||
$renderer[] = $this->pdfRendererFactory->create($tplName, '@export/' . $tplName, $this->filenameToTitle($tplName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +137,28 @@ final class ServiceExport
|
||||
return array_merge($this->renderer, $renderer);
|
||||
}
|
||||
|
||||
private function createTemplateFromExportTemplate(ExportTemplate $template): ExportRendererInterface
|
||||
{
|
||||
$tpl = new Template((string) $template->getId(), $template->getTitle()); // @phpstan-ignore argument.type
|
||||
$tpl->setColumns($template->getColumns());
|
||||
$tpl->setLocale($template->getLanguage());
|
||||
$tpl->setOptions($template->getOptions());
|
||||
|
||||
switch ($template->getRenderer()) {
|
||||
case 'csv':
|
||||
return $this->csvRendererFactory->create($tpl);
|
||||
|
||||
case 'xlsx':
|
||||
return $this->xlsxRendererFactory->create($tpl);
|
||||
|
||||
case 'pdf':
|
||||
return $this->pdfRendererFactory->createFromTemplate($tpl);
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown export template type: ' . $template->getRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
public function getRendererById(string $id): ?ExportRendererInterface
|
||||
{
|
||||
foreach ($this->getRenderer() as $renderer) {
|
||||
@@ -165,6 +180,7 @@ final class ServiceExport
|
||||
*/
|
||||
public function getTimesheetExporter(): array
|
||||
{
|
||||
// TODO 3.0 cache the result, as this is one extra database query on the timesheet pages
|
||||
$exporter = [
|
||||
$this->pdfRendererFactory->create('pdf', '@export/timesheet.pdf.twig'),
|
||||
$this->xlsxRendererFactory->createDefault(),
|
||||
@@ -172,6 +188,17 @@ final class ServiceExport
|
||||
$this->htmlRendererFactory->create('print', 'timesheet/export.html.twig'),
|
||||
];
|
||||
|
||||
foreach ($this->exportTemplateRepository->findAll() as $template) {
|
||||
if (!$template->isAvailableForAll()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$exporter[] = $this->createTemplateFromExportTemplate($template);
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->error('Unknown export template type: ' . $template->getRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($this->timesheetExporter, $exporter);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Form\Type\ExportRendererType;
|
||||
use App\Form\Type\ExportSummaryColumnsType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\PdfFontType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -22,6 +23,9 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
|
||||
/**
|
||||
* TODO rename with 3.0 to ExportTemplateForm
|
||||
*/
|
||||
class ExportTemplateSpreadsheetForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
@@ -77,6 +81,11 @@ class ExportTemplateSpreadsheetForm extends AbstractType
|
||||
'row_attr' => ['data-type' => 'pdf'],
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('availableForAll', YesNoType::class, [
|
||||
'label' => 'user_access_all',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
||||
@@ -19,9 +19,9 @@ final class ExportRendererType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'choices' => [
|
||||
'button.csv' => 'csv',
|
||||
'button.xlsx' => 'xlsx',
|
||||
'button.pdf' => 'pdf'
|
||||
'csv' => 'csv',
|
||||
'xlsx' => 'xlsx',
|
||||
'pdf' => 'pdf'
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -33,8 +33,9 @@ final class TeamType extends AbstractType
|
||||
return $team->getName();
|
||||
},
|
||||
'documentation' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Team ID',
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'integer', 'description' => 'Team IDs'],
|
||||
'description' => 'Array of Team IDs',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
42
src/Twig/SecurityPolicy/DefaultPolicy.php
Normal file
42
src/Twig/SecurityPolicy/DefaultPolicy.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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\Twig\SecurityPolicy;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* @deprecated since 2.47.0 - use StrictPolicy instead
|
||||
*/
|
||||
#[Exclude]
|
||||
final class DefaultPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private SecurityPolicyInterface $securityPolicy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->securityPolicy = new StrictPolicy();
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->securityPolicy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
$this->securityPolicy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->securityPolicy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
42
src/Twig/SecurityPolicy/ExportPolicy.php
Normal file
42
src/Twig/SecurityPolicy/ExportPolicy.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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\Twig\SecurityPolicy;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* @deprecated since 2.47.0 - use StrictPolicy instead
|
||||
*/
|
||||
#[Exclude]
|
||||
final class ExportPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private SecurityPolicyInterface $securityPolicy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->securityPolicy = new StrictPolicy();
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->securityPolicy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
$this->securityPolicy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->securityPolicy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
42
src/Twig/SecurityPolicy/InvoicePolicy.php
Normal file
42
src/Twig/SecurityPolicy/InvoicePolicy.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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\Twig\SecurityPolicy;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* @deprecated since 2.47.0 - use StrictPolicy instead
|
||||
*/
|
||||
#[Exclude]
|
||||
final class InvoicePolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private SecurityPolicyInterface $securityPolicy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->securityPolicy = new StrictPolicy();
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->securityPolicy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
$this->securityPolicy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->securityPolicy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,6 @@ final class UserDurationYear extends AbstractCounterYear
|
||||
|
||||
protected function getFinancialYearTitle(): string
|
||||
{
|
||||
return 'stats.durationFinancialYear';
|
||||
return 'stats.userDurationYear';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ final class DayAddon
|
||||
{
|
||||
private bool $billable = true;
|
||||
|
||||
public function __construct(private string $title, private int $duration, private int $visibleDuration)
|
||||
public function __construct(
|
||||
private readonly string $title,
|
||||
private readonly int $duration,
|
||||
private readonly int $visibleDuration,
|
||||
private readonly ?string $type = null
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -41,4 +46,9 @@ final class DayAddon
|
||||
{
|
||||
return $this->visibleDuration;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user