mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-06-17 08:01:32 +00:00
Avoid using render in datatables, as it require escaping
This commit also fixes an XSS vulnerability in IPN project bom
This commit is contained in:
parent
11b41ee66a
commit
dfbdac7688
7 changed files with 150 additions and 88 deletions
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\DataTables;
|
||||
|
||||
use App\DataTables\Column\HTMLColumn;
|
||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||
use App\DataTables\Column\PrettyBoolColumn;
|
||||
use App\DataTables\Column\RowClassColumn;
|
||||
|
|
@ -40,14 +41,19 @@ use Omines\DataTablesBundle\DataTable;
|
|||
use Omines\DataTablesBundle\DataTableTypeInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class AttachmentDataTable implements DataTableTypeInterface
|
||||
final readonly class AttachmentDataTable implements DataTableTypeInterface
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator, private readonly AttachmentManager $attachmentHelper, private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly ElementTypeNameGenerator $elementTypeNameGenerator)
|
||||
public function __construct(private TranslatorInterface $translator, private EntityURLGenerator $entityURLGenerator, private AttachmentManager $attachmentHelper, private AttachmentURLGenerator $attachmentURLGenerator, private ElementTypeNameGenerator $elementTypeNameGenerator)
|
||||
{
|
||||
}
|
||||
|
||||
public function configure(DataTable $dataTable, array $options): void
|
||||
{
|
||||
/*************************************************************************************************************
|
||||
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||
* HTMLColumn, if necessary
|
||||
************************************************************************************************************/
|
||||
|
||||
$dataTable->add('dont_matter', RowClassColumn::class, [
|
||||
'render' => function ($value, Attachment $context): string {
|
||||
//Mark attachments yellow which have an internal file linked that doesn't exist
|
||||
|
|
@ -59,10 +65,10 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
},
|
||||
]);
|
||||
|
||||
$dataTable->add('picture', TextColumn::class, [
|
||||
$dataTable->add('picture', HTMLColumn::class, [
|
||||
'label' => '',
|
||||
'className' => 'no-colvis',
|
||||
'render' => function ($value, Attachment $context): string {
|
||||
'data' => function (Attachment $context): string {
|
||||
if ($context->isPicture()
|
||||
&& $this->attachmentHelper->isInternalFileExisting($context)) {
|
||||
|
||||
|
|
@ -95,65 +101,65 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
'orderField' => 'NATSORT(attachment.name)',
|
||||
]);
|
||||
|
||||
$dataTable->add('attachment_type', TextColumn::class, [
|
||||
$dataTable->add('attachment_type', HTMLColumn::class, [
|
||||
'label' => 'attachment.table.type',
|
||||
'field' => 'attachment_type.name',
|
||||
'orderField' => 'NATSORT(attachment_type.name)',
|
||||
'render' => fn($value, Attachment $context): string => sprintf(
|
||||
'data' => fn(Attachment $context, $value): string => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
$this->entityURLGenerator->editURL($context->getAttachmentType()),
|
||||
htmlspecialchars((string) $value)
|
||||
),
|
||||
]);
|
||||
|
||||
$dataTable->add('element', TextColumn::class, [
|
||||
$dataTable->add('element', HTMLColumn::class, [
|
||||
'label' => 'attachment.table.element',
|
||||
//'propertyPath' => 'element.name',
|
||||
'render' => fn($value, Attachment $context): string => sprintf(
|
||||
'data' => fn(Attachment $context): string => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
$this->entityURLGenerator->infoURL($context->getElement()),
|
||||
$this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true)
|
||||
),
|
||||
]);
|
||||
|
||||
$dataTable->add('internal_link', TextColumn::class, [
|
||||
$dataTable->add('internal_link', HTMLColumn::class, [
|
||||
'label' => 'attachment.table.internal_file',
|
||||
'propertyPath' => 'filename',
|
||||
'orderField' => 'NATSORT(attachment.original_filename)',
|
||||
'render' => function ($value, Attachment $context) {
|
||||
'data' => function (Attachment $context, $value) {
|
||||
if ($this->attachmentHelper->isInternalFileExisting($context)) {
|
||||
return sprintf(
|
||||
'<a href="%s" target="_blank" data-no-ajax>%s</a>',
|
||||
$this->entityURLGenerator->viewURL($context),
|
||||
htmlspecialchars($value)
|
||||
htmlspecialchars((string) $value)
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
return htmlspecialchars((string) $value);
|
||||
},
|
||||
]);
|
||||
|
||||
$dataTable->add('external_link', TextColumn::class, [
|
||||
$dataTable->add('external_link', HTMLColumn::class, [
|
||||
'label' => 'attachment.table.external_link',
|
||||
'propertyPath' => 'host',
|
||||
'orderField' => 'attachment.external_path',
|
||||
'render' => function ($value, Attachment $context) {
|
||||
'data' => function (Attachment $context, $value) {
|
||||
if ($context->hasExternal()) {
|
||||
return sprintf(
|
||||
'<a href="%s" class="link-external" title="%s" target="_blank" rel="noopener">%s</a>',
|
||||
htmlspecialchars((string) $context->getExternalPath()),
|
||||
htmlspecialchars((string) $context->getExternalPath()),
|
||||
htmlspecialchars($value),
|
||||
htmlspecialchars((string) $value),
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
return htmlspecialchars((string) $value);
|
||||
},
|
||||
]);
|
||||
|
||||
$dataTable->add('filesize', TextColumn::class, [
|
||||
$dataTable->add('filesize', HTMLColumn::class, [
|
||||
'label' => $this->translator->trans('attachment.table.filesize'),
|
||||
'render' => function ($value, Attachment $context) {
|
||||
'data' => function (Attachment $context) {
|
||||
if (!$context->hasInternal()) {
|
||||
return sprintf(
|
||||
'<span class="badge bg-primary">
|
||||
|
|
@ -168,7 +174,7 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
'<span class="badge bg-secondary">
|
||||
<i class="fas fa-hdd fa-fw"></i> %s
|
||||
</span>',
|
||||
$this->attachmentHelper->getHumanFileSize($context)
|
||||
htmlspecialchars($this->attachmentHelper->getHumanFileSize($context))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
37
src/DataTables/Column/HTMLColumn.php
Normal file
37
src/DataTables/Column/HTMLColumn.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace App\DataTables\Column;
|
||||
|
||||
use Omines\DataTablesBundle\Column\TextColumn;
|
||||
|
||||
/**
|
||||
* A TextColumn whose value is always treated as raw HTML and therefore never passed through htmlspecialchars().
|
||||
* The value returned by the 'data' option must already contain properly escaped/sanitized HTML, as it is output as-is.
|
||||
*/
|
||||
class HTMLColumn extends TextColumn
|
||||
{
|
||||
public function isRaw(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -22,9 +22,9 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace App\DataTables;
|
||||
|
||||
use App\DataTables\Column\HTMLColumn;
|
||||
use App\DataTables\Column\RowClassColumn;
|
||||
use Omines\DataTablesBundle\Adapter\ArrayAdapter;
|
||||
use Omines\DataTablesBundle\Column\TextColumn;
|
||||
use Omines\DataTablesBundle\DataTable;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
use Omines\DataTablesBundle\DataTableTypeInterface;
|
||||
|
|
@ -32,7 +32,7 @@ use Symfony\Component\HttpFoundation\Request;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ErrorDataTable implements DataTableTypeInterface
|
||||
final readonly class ErrorDataTable implements DataTableTypeInterface
|
||||
{
|
||||
public function configureOptions(OptionsResolver $optionsResolver): void
|
||||
{
|
||||
|
|
@ -49,6 +49,11 @@ class ErrorDataTable implements DataTableTypeInterface
|
|||
|
||||
public function configure(DataTable $dataTable, array $options): void
|
||||
{
|
||||
/*************************************************************************************************************
|
||||
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||
* HTMLColumn, if necessary
|
||||
************************************************************************************************************/
|
||||
|
||||
$optionsResolver = new OptionsResolver();
|
||||
$this->configureOptions($optionsResolver);
|
||||
$options = $optionsResolver->resolve($options);
|
||||
|
|
@ -58,9 +63,9 @@ class ErrorDataTable implements DataTableTypeInterface
|
|||
'render' => fn($value, $context): string => 'table-warning',
|
||||
])
|
||||
|
||||
->add('error', TextColumn::class, [
|
||||
->add('error', HTMLColumn::class, [
|
||||
'label' => 'error_table.error',
|
||||
'render' => fn($value, $context): string => '<i class="fa-solid fa-triangle-exclamation fa-fw"></i> ' . $value,
|
||||
'data' => fn($context, $value): string => '<i class="fa-solid fa-triangle-exclamation fa-fw"></i> ' . htmlspecialchars((string) $value),
|
||||
])
|
||||
;
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class PartDataTableHelper
|
|||
}
|
||||
if ($context->getBuiltProject() instanceof Project) {
|
||||
$icon = sprintf('<i class="fa-solid fa-box-archive fa-fw me-1" title="%s"></i>',
|
||||
$this->translator->trans('part.info.projectBuildPart.hint').': '.$context->getBuiltProject()->getName());
|
||||
$this->translator->trans('part.info.projectBuildPart.hint').': '.htmlspecialchars($context->getBuiltProject()->getName()));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ namespace App\DataTables;
|
|||
use App\DataTables\Column\EnumColumn;
|
||||
use App\Entity\LogSystem\LogTargetType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\DataTables\Column\HTMLColumn;
|
||||
use App\DataTables\Column\IconLinkColumn;
|
||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||
use App\DataTables\Column\LogEntryExtraColumn;
|
||||
|
|
@ -59,7 +60,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LogDataTable implements DataTableTypeInterface
|
||||
final readonly class LogDataTable implements DataTableTypeInterface
|
||||
{
|
||||
protected LogEntryRepository $logRepo;
|
||||
|
||||
|
|
@ -95,6 +96,11 @@ class LogDataTable implements DataTableTypeInterface
|
|||
|
||||
public function configure(DataTable $dataTable, array $options): void
|
||||
{
|
||||
/*************************************************************************************************************
|
||||
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||
* HTMLColumn, if necessary
|
||||
************************************************************************************************************/
|
||||
|
||||
$resolver = new OptionsResolver();
|
||||
$this->configureOptions($resolver);
|
||||
$options = $resolver->resolve($options);
|
||||
|
|
@ -104,10 +110,10 @@ class LogDataTable implements DataTableTypeInterface
|
|||
'render' => fn($value, AbstractLogEntry $context) => $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString()),
|
||||
]);
|
||||
|
||||
$dataTable->add('symbol', TextColumn::class, [
|
||||
$dataTable->add('symbol', HTMLColumn::class, [
|
||||
'label' => '',
|
||||
'className' => 'no-colvis',
|
||||
'render' => fn($value, AbstractLogEntry $context): string => sprintf(
|
||||
'data' => fn(AbstractLogEntry $context): string => sprintf(
|
||||
'<i class="fas fa-fw %s" title="%s"></i>',
|
||||
$this->logLevelHelper->logLevelToIconClass($context->getLevelString()),
|
||||
$context->getLevelString()
|
||||
|
|
@ -128,10 +134,10 @@ class LogDataTable implements DataTableTypeInterface
|
|||
)
|
||||
]);
|
||||
|
||||
$dataTable->add('type', TextColumn::class, [
|
||||
$dataTable->add('type', HTMLColumn::class, [
|
||||
'label' => 'log.type',
|
||||
'propertyPath' => 'type',
|
||||
'render' => function (string $value, AbstractLogEntry $context) {
|
||||
'data' => function (AbstractLogEntry $context, string $value) {
|
||||
$text = $this->translator->trans('log.type.'.$value);
|
||||
|
||||
if ($context instanceof PartStockChangedLogEntry) {
|
||||
|
|
@ -149,20 +155,20 @@ class LogDataTable implements DataTableTypeInterface
|
|||
'label' => 'log.level',
|
||||
'visible' => 'system_log' === $options['mode'],
|
||||
'propertyPath' => 'levelString',
|
||||
'render' => fn(string $value, AbstractLogEntry $context) => $this->translator->trans('log.level.'.$value),
|
||||
'data' => fn(AbstractLogEntry $context, string $value) => $this->translator->trans('log.level.'.$value),
|
||||
]);
|
||||
|
||||
$dataTable->add('user', TextColumn::class, [
|
||||
$dataTable->add('user', HTMLColumn::class, [
|
||||
'label' => 'log.user',
|
||||
'orderField' => 'NATSORT(user.name)',
|
||||
'render' => function ($value, AbstractLogEntry $context): string {
|
||||
'data' => function (AbstractLogEntry $context): string {
|
||||
$user = $context->getUser();
|
||||
|
||||
//If user was deleted, show the info from the username field
|
||||
if (!$user instanceof User) {
|
||||
if ($context->isCLIEntry()) {
|
||||
return sprintf('%s [%s]',
|
||||
htmlentities((string) $context->getCLIUsername()),
|
||||
htmlspecialchars((string) $context->getCLIUsername()),
|
||||
$this->translator->trans('log.cli_user')
|
||||
);
|
||||
}
|
||||
|
|
@ -170,7 +176,7 @@ class LogDataTable implements DataTableTypeInterface
|
|||
//Else we just deal with a deleted user
|
||||
return sprintf(
|
||||
'@%s [%s]',
|
||||
htmlentities($context->getUsername()),
|
||||
htmlspecialchars($context->getUsername()),
|
||||
$this->translator->trans('log.target_deleted'),
|
||||
);
|
||||
}
|
||||
|
|
@ -182,7 +188,7 @@ class LogDataTable implements DataTableTypeInterface
|
|||
$img_url,
|
||||
$this->userAvatarHelper->getAvatarMdURL($user),
|
||||
$this->urlGenerator->generate('user_info', ['id' => $user->getID()]),
|
||||
htmlentities($user->getFullName(true))
|
||||
htmlspecialchars($user->getFullName(true))
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
@ -194,7 +200,7 @@ class LogDataTable implements DataTableTypeInterface
|
|||
'render' => function (LogTargetType $value, AbstractLogEntry $context) {
|
||||
$class = $value->toClass();
|
||||
if (null !== $class) {
|
||||
return $this->elementTypeNameGenerator->getLocalizedTypeLabel($class);
|
||||
return $this->elementTypeNameGenerator->typeLabel($class);
|
||||
}
|
||||
|
||||
return '';
|
||||
|
|
@ -216,9 +222,9 @@ class LogDataTable implements DataTableTypeInterface
|
|||
'icon' => 'fas fa-fw fa-eye',
|
||||
'href' => function ($value, AbstractLogEntry $context) {
|
||||
if (
|
||||
$context instanceof CollectionElementDeleted ||
|
||||
($context instanceof TimeTravelInterface
|
||||
&& $context->hasOldDataInformation())
|
||||
|| $context instanceof CollectionElementDeleted
|
||||
) {
|
||||
try {
|
||||
$target = $this->logRepo->getTargetElement($context);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ namespace App\DataTables;
|
|||
use App\DataTables\Adapters\TwoStepORMAdapter;
|
||||
use App\DataTables\Column\EntityColumn;
|
||||
use App\DataTables\Column\EnumColumn;
|
||||
use App\DataTables\Column\HTMLColumn;
|
||||
use App\DataTables\Column\IconLinkColumn;
|
||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||
use App\DataTables\Column\MarkdownColumn;
|
||||
|
|
@ -58,7 +59,7 @@ use Symfony\Bundle\SecurityBundle\Security;
|
|||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class PartsDataTable implements DataTableTypeInterface
|
||||
final readonly class PartsDataTable implements DataTableTypeInterface
|
||||
{
|
||||
public const LENGTH_MENU = [[10, 25, 50, 100, 250, 500, -1], [10, 25, 50, 100, 250, 500, "All"]];
|
||||
|
||||
|
|
@ -94,6 +95,11 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
* When adding columns here, add them also to PartTableColumns enum, to make them configurable in the settings!
|
||||
*************************************************************************************************************/
|
||||
|
||||
/*************************************************************************************************************
|
||||
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||
* HTMLColumn, if necessary
|
||||
************************************************************************************************************/
|
||||
|
||||
$this->csh
|
||||
//Color the table rows depending on the review and favorite status
|
||||
->add('row_color', RowClassColumn::class, [
|
||||
|
|
@ -109,23 +115,23 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
},
|
||||
], visibility_configurable: false)
|
||||
->add('select', SelectColumn::class, visibility_configurable: false)
|
||||
->add('picture', TextColumn::class, [
|
||||
->add('picture', HTMLColumn::class, [
|
||||
'label' => '',
|
||||
'className' => 'no-colvis',
|
||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderPicture($context),
|
||||
'data' => fn(Part $context) => $this->partDataTableHelper->renderPicture($context),
|
||||
], visibility_configurable: false)
|
||||
->add('name', TextColumn::class, [
|
||||
->add('name', HTMLColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.name'),
|
||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderName($context),
|
||||
'data' => fn(Part $context) => $this->partDataTableHelper->renderName($context),
|
||||
'orderField' => 'NATSORT(part.name)'
|
||||
])
|
||||
->add('si_value', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.si_value'),
|
||||
'render' => function ($value, Part $context): string {
|
||||
'data' => function (Part $context): string {
|
||||
$siValue = SiValueSort::sqliteSiValue($context->getName());
|
||||
if ($siValue !== null) {
|
||||
//Output it as scientific number with a big E
|
||||
return htmlspecialchars(sprintf('%G', $siValue));
|
||||
return sprintf('%G', $siValue);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
|
@ -156,38 +162,38 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
'label' => $this->translator->trans('part.table.manufacturer'),
|
||||
'orderField' => 'NATSORT(_manufacturer.name)'
|
||||
])
|
||||
->add('storelocation', TextColumn::class, [
|
||||
->add('storelocation', HTMLColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.storeLocations'),
|
||||
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
|
||||
'orderField' => 'NATSORT(MIN(_storelocations.name))',
|
||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
|
||||
'data' => fn(Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
|
||||
], alias: 'storage_location')
|
||||
|
||||
->add('amount', TextColumn::class, [
|
||||
->add('amount', HTMLColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.amount'),
|
||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderAmount($context),
|
||||
'data' => fn(Part $context) => $this->partDataTableHelper->renderAmount($context),
|
||||
'orderField' => 'amountSum'
|
||||
])
|
||||
->add('minamount', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.minamount'),
|
||||
'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format(
|
||||
'data' => fn(Part $context, $value): string => $this->amountFormatter->format(
|
||||
$value,
|
||||
$context->getPartUnit()
|
||||
)),
|
||||
),
|
||||
])
|
||||
->add('partUnit', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.partUnit'),
|
||||
'orderField' => 'NATSORT(_partUnit.name)',
|
||||
'render' => function ($value, Part $context): string {
|
||||
'data' => function (Part $context): string {
|
||||
$partUnit = $context->getPartUnit();
|
||||
if ($partUnit === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$tmp = htmlspecialchars($partUnit->getName());
|
||||
$tmp = $partUnit->getName();
|
||||
|
||||
if ($partUnit->getUnit()) {
|
||||
$tmp .= ' (' . htmlspecialchars($partUnit->getUnit()) . ')';
|
||||
$tmp .= ' (' . $partUnit->getUnit() . ')';
|
||||
}
|
||||
return $tmp;
|
||||
}
|
||||
|
|
@ -195,14 +201,14 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
->add('partCustomState', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.partCustomState'),
|
||||
'orderField' => 'NATSORT(_partCustomState.name)',
|
||||
'render' => function($value, Part $context): string {
|
||||
'data' => function(Part $context): string {
|
||||
$partCustomState = $context->getPartCustomState();
|
||||
|
||||
if ($partCustomState === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return htmlspecialchars($partCustomState->getName());
|
||||
return $partCustomState->getName();
|
||||
}
|
||||
])
|
||||
->add('addedDate', LocaleDateTimeColumn::class, [
|
||||
|
|
@ -248,25 +254,25 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
])
|
||||
->add('eda_reference', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.eda_reference'),
|
||||
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getReferencePrefix() ?? ''),
|
||||
'data' => static fn(Part $context) => $context->getEdaInfo()->getReferencePrefix() ?? '',
|
||||
'orderField' => 'NATSORT(part.eda_info.reference_prefix)'
|
||||
])
|
||||
->add('eda_value', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.eda_value'),
|
||||
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getValue() ?? ''),
|
||||
'data' => static fn(Part $context) => $context->getEdaInfo()->getValue() ?? '',
|
||||
'orderField' => 'NATSORT(part.eda_info.value)'
|
||||
])
|
||||
->add('eda_status', TextColumn::class, [
|
||||
->add('eda_status', HTMLColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.eda_status'),
|
||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderEdaStatus($context),
|
||||
'data' => fn(Part $context) => $this->partDataTableHelper->renderEdaStatus($context),
|
||||
'className' => 'text-center',
|
||||
]);
|
||||
|
||||
//Add a column to list the projects where the part is used, when the user has the permission to see the projects
|
||||
if ($this->security->isGranted('read', Project::class)) {
|
||||
$this->csh->add('projects', TextColumn::class, [
|
||||
$this->csh->add('projects', HTMLColumn::class, [
|
||||
'label' => $this->translator->trans('project.labelp'),
|
||||
'render' => function ($value, Part $context): string {
|
||||
'data' => function (Part $context): string {
|
||||
//Only show the first 5 projects names
|
||||
$projects = $context->getProjects();
|
||||
$tmp = "";
|
||||
|
|
@ -286,7 +292,7 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
}
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ namespace App\DataTables;
|
|||
use App\DataTables\Adapters\TwoStepORMAdapter;
|
||||
use App\DataTables\Column\EntityColumn;
|
||||
use App\DataTables\Column\EnumColumn;
|
||||
use App\DataTables\Column\HTMLColumn;
|
||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||
use App\DataTables\Column\MarkdownColumn;
|
||||
use App\DataTables\Helpers\PartDataTableHelper;
|
||||
|
|
@ -48,7 +49,7 @@ use Omines\DataTablesBundle\DataTable;
|
|||
use Omines\DataTablesBundle\DataTableTypeInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||
final readonly class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected EntityURLGenerator $entityURLGenerator,
|
||||
|
|
@ -63,17 +64,22 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
|
||||
public function configure(DataTable $dataTable, array $options): void
|
||||
{
|
||||
/*************************************************************************************************************
|
||||
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||
* HTMLColumn, if necessary
|
||||
************************************************************************************************************/
|
||||
|
||||
$dataTable
|
||||
//->add('select', SelectColumn::class)
|
||||
->add('picture', TextColumn::class, [
|
||||
->add('picture', HTMLColumn::class, [
|
||||
'label' => '',
|
||||
'className' => 'no-colvis',
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
'data' => function (ProjectBOMEntry $context) {
|
||||
if(!$context->getPart() instanceof Part) {
|
||||
return '';
|
||||
}
|
||||
return $this->partDataTableHelper->renderPicture($context->getPart());
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
->add('id', TextColumn::class, [
|
||||
|
|
@ -85,27 +91,27 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
'label' => $this->translator->trans('project.bom.quantity'),
|
||||
'className' => 'text-center',
|
||||
'orderField' => 'bom_entry.quantity',
|
||||
'render' => function ($value, ProjectBOMEntry $context): float|string {
|
||||
'data' => function (ProjectBOMEntry $context): float|string {
|
||||
//If we have a non-part entry, only show the rounded quantity
|
||||
if (!$context->getPart() instanceof Part) {
|
||||
return round($context->getQuantity());
|
||||
}
|
||||
//Otherwise use the unit of the part to format the quantity
|
||||
return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit()));
|
||||
return $this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit());
|
||||
},
|
||||
])
|
||||
->add('partId', TextColumn::class, [
|
||||
'label' => $this->translator->trans('project.bom.part_id'),
|
||||
'visible' => true,
|
||||
'orderField' => 'part.id',
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
'data' => function (ProjectBOMEntry $context) {
|
||||
return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : '';
|
||||
},
|
||||
])
|
||||
->add('name', TextColumn::class, [
|
||||
->add('name', HTMLColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.name'),
|
||||
'orderField' => 'NATSORT(part.name)',
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
'data' => function (ProjectBOMEntry $context) {
|
||||
if(!$context->getPart() instanceof Part) {
|
||||
return htmlspecialchars((string) $context->getName());
|
||||
}
|
||||
|
|
@ -123,11 +129,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
'label' => $this->translator->trans('part.table.ipn'),
|
||||
'orderField' => 'NATSORT(part.ipn)',
|
||||
'visible' => false,
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
if($context->getPart() instanceof Part) {
|
||||
return $context->getPart()->getIpn();
|
||||
}
|
||||
}
|
||||
'data' => fn (ProjectBOMEntry $context) => $context->getPart()?->getIpn()
|
||||
])
|
||||
->add('description', MarkdownColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.description'),
|
||||
|
|
@ -172,9 +174,9 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
},
|
||||
])
|
||||
|
||||
->add('mountnames', TextColumn::class, [
|
||||
->add('mountnames', HTMLColumn::class, [
|
||||
'label' => 'project.bom.mountnames',
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
'data' => function (ProjectBOMEntry $context) {
|
||||
$html = '';
|
||||
|
||||
foreach (explode(',', $context->getMountnames()) as $mountname) {
|
||||
|
|
@ -184,34 +186,34 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
},
|
||||
])
|
||||
|
||||
->add('instockAmount', TextColumn::class, [
|
||||
->add('instockAmount', HTMLColumn::class, [
|
||||
'label' => 'project.bom.instockAmount',
|
||||
'visible' => false,
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
'data' => function (ProjectBOMEntry $context) {
|
||||
if ($context->getPart() !== null) {
|
||||
return $this->partDataTableHelper->renderAmount($context->getPart());
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
])
|
||||
->add('storelocation', TextColumn::class, [
|
||||
->add('storelocation', HTMLColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.storeLocations'),
|
||||
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
|
||||
'orderField' => 'NATSORT(MIN(_storelocations.name))',
|
||||
'visible' => false,
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
'data' => function (ProjectBOMEntry $context) {
|
||||
if ($context->getPart() !== null) {
|
||||
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
])
|
||||
->add('price', TextColumn::class, [
|
||||
'label' => 'project.bom.price',
|
||||
'visible' => false,
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
'data' => function (ProjectBOMEntry $context) {
|
||||
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
|
||||
return $this->moneyFormatter->format($price->toScale(2, RoundingMode::Up)->toFloat(), null, 2, true);
|
||||
},
|
||||
|
|
@ -219,7 +221,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
->add('ext_price', TextColumn::class, [
|
||||
'label' => 'project.bom.ext_price',
|
||||
'visible' => false,
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
'data' => function (ProjectBOMEntry $context) {
|
||||
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
|
||||
return $this->moneyFormatter->format(
|
||||
$price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity()))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue