diff --git a/src/DataTables/AttachmentDataTable.php b/src/DataTables/AttachmentDataTable.php index 16e6a7a7..6c4c905a 100644 --- a/src/DataTables/AttachmentDataTable.php +++ b/src/DataTables/AttachmentDataTable.php @@ -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( '%s', $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( '%s', $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( '%s', $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( '%s', 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( ' @@ -168,7 +174,7 @@ final class AttachmentDataTable implements DataTableTypeInterface ' %s ', - $this->attachmentHelper->getHumanFileSize($context) + htmlspecialchars($this->attachmentHelper->getHumanFileSize($context)) ); } diff --git a/src/DataTables/Column/HTMLColumn.php b/src/DataTables/Column/HTMLColumn.php new file mode 100644 index 00000000..a1220dd3 --- /dev/null +++ b/src/DataTables/Column/HTMLColumn.php @@ -0,0 +1,37 @@ +. + */ +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; + } +} diff --git a/src/DataTables/ErrorDataTable.php b/src/DataTables/ErrorDataTable.php index 833ea934..a16b453e 100644 --- a/src/DataTables/ErrorDataTable.php +++ b/src/DataTables/ErrorDataTable.php @@ -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 => ' ' . $value, + 'data' => fn($context, $value): string => ' ' . htmlspecialchars((string) $value), ]) ; diff --git a/src/DataTables/Helpers/PartDataTableHelper.php b/src/DataTables/Helpers/PartDataTableHelper.php index 54094ff1..2f40dbd2 100644 --- a/src/DataTables/Helpers/PartDataTableHelper.php +++ b/src/DataTables/Helpers/PartDataTableHelper.php @@ -62,7 +62,7 @@ class PartDataTableHelper } if ($context->getBuiltProject() instanceof Project) { $icon = sprintf('', - $this->translator->trans('part.info.projectBuildPart.hint').': '.$context->getBuiltProject()->getName()); + $this->translator->trans('part.info.projectBuildPart.hint').': '.htmlspecialchars($context->getBuiltProject()->getName())); } diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index 2c37767b..5c4ca88b 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -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( '', $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); diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index bcf64056..5912a20e 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -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; - } + }, ]); } diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index b5beeca0..f65f0df7 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -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()))