TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,515 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Form\Container;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceControlsEvent;
use TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceEnabledControlsEvent;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Render a single file reference.
*
* This container is called by FilesControlContainer to render a single file (reference). The container is also
* called by FormEngine for an incoming ajax request to expand an existing file (reference) or to create a new one.
*
* This container creates the outer HTML of single file (references) - e.g. drag and drop and delete buttons.
* For rendering of the record itself processing is handed over to FullRecordContainer.
*/
class FileReferenceContainer extends AbstractContainer
{
private const string FILE_REFERENCE_TABLE = 'sys_file_reference';
private const string FOREIGN_SELECTOR = 'uid_local';
/**
* File reference data used for JSON output
*/
protected array $fileReferenceData = [];
public function __construct(
private readonly IconFactory $iconFactory,
private readonly InlineStackProcessor $inlineStackProcessor,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly ResourceFactory $resourceFactory,
private readonly ConnectionPool $connectionPool,
private readonly UriBuilder $uriBuilder,
) {}
public function render(): array
{
// Send a mapping information to the browser via JSON:
// e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
$formPrefix = $this->inlineStackProcessor->getFormPrefixFromStructure($this->data['inlineStructure']);
$domObjectId = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $this->data['inlineFirstPid']);
$this->fileReferenceData = $this->data['inlineData'];
$this->fileReferenceData['map'][$formPrefix] = $domObjectId;
$resultArray = $this->initializeResultArray();
$resultArray['inlineData'] = $this->fileReferenceData;
$html = '';
$classes = [];
$combinationHtml = '';
$record = $this->data['databaseRow'];
$uid = $record['uid'] ?? 0;
$appendFormFieldNames = '[' . self::FILE_REFERENCE_TABLE . '][' . $uid . ']';
$objectId = $domObjectId . '-' . self::FILE_REFERENCE_TABLE . '-' . $uid;
$isNewRecord = $this->data['command'] === 'new';
$hiddenFieldName = (string)($this->data['processedTca']['ctrl']['enablecolumns']['disabled'] ?? '');
if (!$this->data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
if ($isNewRecord || $this->data['isInlineChildExpanded']) {
$fileReferenceData = $this->renderFileReference($this->data);
$html = $fileReferenceData['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fileReferenceData, false);
} else {
// This class is the marker for the JS-function to check if the full content has already been loaded
$classes[] = 't3js-not-loaded';
}
if ($isNewRecord) {
// Add pid of file reference as hidden field
$html .= '<input type="hidden" name="data' . htmlspecialchars($appendFormFieldNames)
. '[pid]" value="' . htmlspecialchars((string)$record['pid']) . '"/>';
// Tell DataHandler this file reference is expanded
$ucFieldName = 'uc[inlineView]'
. '[' . $this->data['inlineTopMostParentTableName'] . ']'
. '[' . $this->data['inlineTopMostParentUid'] . ']'
. htmlspecialchars($appendFormFieldNames);
$html .= '<input type="hidden" name="' . htmlspecialchars($ucFieldName)
. '" value="' . (int)$this->data['isInlineChildExpanded'] . '" />';
} else {
// Set additional field for processing for saving
$html .= '<input type="hidden" name="cmd' . htmlspecialchars($appendFormFieldNames)
. '[delete]" value="1" disabled="disabled" />';
if ($hiddenFieldName !== ''
&& (!$this->data['isInlineChildExpanded']
|| !in_array($hiddenFieldName, $this->data['columnsToProcess'], true))
) {
$isHidden = (bool)($record[$hiddenFieldName] ?? false);
$html .= '<input type="checkbox" class="d-none" data-formengine-input-name="data'
. htmlspecialchars($appendFormFieldNames)
. '[' . htmlspecialchars($hiddenFieldName) . ']" value="1"'
. ($isHidden ? ' checked="checked"' : '') . ' />';
$html .= '<input type="input" class="d-none" name="data' . htmlspecialchars($appendFormFieldNames)
. '[' . htmlspecialchars($hiddenFieldName) . ']" value="' . (int)$isHidden . '" />';
}
}
}
if ($this->data['inlineParentConfig']['renderFieldsOnly'] ?? false) {
// Render "body" part only
$resultArray['html'] = $html . $combinationHtml;
return $resultArray;
}
// Render header row and content (if expanded)
if ($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$classes[] = 'panel-placeholder';
}
if ($record[$hiddenFieldName] ?? false) {
$classes[] = 'panel-hidden';
}
if ($isNewRecord) {
$classes[] = 'inlineIsNewRecord';
}
// The hashed object id needs a non-numeric prefix, the value is used as ID selector in JavaScript
$hashedObjectId = 'hash-' . md5($objectId);
$containerAttributes = [
'id' => $objectId . '_div',
'class' => 'form-irre-object panel panel-default ' . trim(implode(' ', $classes)),
'data-object-uid' => $record['uid'] ?? 0,
'data-object-id' => $objectId,
'data-object-id-hash' => $hashedObjectId,
'data-object-parent-group' => $domObjectId . '-' . self::FILE_REFERENCE_TABLE,
'data-field-name' => $appendFormFieldNames,
'data-topmost-parent-table' => $this->data['inlineTopMostParentTableName'],
'data-topmost-parent-uid' => $this->data['inlineTopMostParentUid'],
'data-placeholder-record' => $this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ? '1' : '0',
];
$isExpanded = $this->data['isInlineChildExpanded'] ?? false;
$ariaControls = htmlspecialchars($objectId . '_fields', ENT_QUOTES | ENT_HTML5);
$resultArray['html'] = '
<div ' . GeneralUtility::implodeAttributes($containerAttributes, true) . '>
<div class="panel-heading">
<div class="panel-heading-row">
' . $this->renderFileHeader($isExpanded, $ariaControls) . '
</div>
</div>
<div class="panel-collapse collapse' . ($isExpanded ? ' show' : '') . '" id="' . $ariaControls . '">' . $html . $combinationHtml . '</div>
</div>';
return $resultArray;
}
protected function renderFileReference(array $data): array
{
$data['tabAndInlineStack'][] = [
'inline',
$this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $data['inlineFirstPid'])
. '-'
. $data['tableName']
. '-'
. $data['databaseRow']['uid'],
];
return $this->nodeFactory->create(array_replace_recursive($data, [
'inlineData' => $this->fileReferenceData,
'renderType' => 'fullRecordContainer',
]))->render();
}
/**
* Renders the HTML header for the file, such as the title, toggle-function, drag'n'drop, etc.
* Later on the command-icons are inserted here, too.
*/
protected function renderFileHeader(bool $isExpanded, string $ariaControls): string
{
$languageService = $this->getLanguageService();
$databaseRow = $this->data['databaseRow'];
$recordTitle = $this->getRecordTitle();
if (empty($recordTitle)) {
$recordTitle = '<em>[' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>';
}
$objectId = htmlspecialchars($this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $this->data['inlineFirstPid'])
. '-' . self::FILE_REFERENCE_TABLE
. '-' . ($databaseRow['uid'] ?? 0));
$altText = BackendUtility::getRecordIconAltText($databaseRow, self::FILE_REFERENCE_TABLE, false);
// Renders the header image (thumbnail, icon, or missing file indicator)
$headerImage = '';
$headerBadge = '';
$isMissing = false;
if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] ?? false) {
$fileUid = $databaseRow[self::FOREIGN_SELECTOR][0]['uid'] ?? null;
if (!empty($fileUid)) {
try {
$fileObject = $this->resourceFactory->getFileObject($fileUid);
if ($fileObject->isMissing()) {
$isMissing = true;
$recordTitle = htmlspecialchars($fileObject->getName());
$headerImage = '
<div class="panel-icon" id="' . $objectId . '_iconcontainer">
' . $this->iconFactory->getIcon('default-not-found', IconSize::SMALL)->render() . '
</div>';
$headerBadge = '
<div class="panel-badge">
<span class="badge badge-danger">'
. htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing')) . '
</span>
</div>';
} elseif ($fileObject->isImage() || $fileObject->isMediaFile()) {
$imageSetup = $this->data['inlineParentConfig']['appearance']['headerThumbnail'] ?? [];
$cropVariantCollection = CropVariantCollection::create($databaseRow['crop'] ?? '');
if (!$cropVariantCollection->getCropArea()->isEmpty()) {
$imageSetup['crop'] = $cropVariantCollection->getCropArea()->makeAbsoluteBasedOnFile($fileObject);
}
$processedImage = $fileObject->process(
ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
array_merge(['maxWidth' => 60, 'maxHeight' => 45], $imageSetup)
);
// Only use a thumbnail if the processing process was successful by checking if image width is set
if ($processedImage->getProperty('width')) {
$imageUrl = $processedImage->getPublicUrl() ?? '';
$headerImage = '
<div class="panel-thumbnail" id="' . $objectId . '_thumbnailcontainer">
<img src="' . htmlspecialchars($imageUrl) . '" '
. 'width="' . $processedImage->getProperty('width') . '" '
. 'height="' . $processedImage->getProperty('height') . '" '
. 'alt="" '
. 'title="' . htmlspecialchars($altText) . '" '
. 'loading="lazy">
</div>';
}
}
} catch (\InvalidArgumentException $e) {
$fileObject = null;
}
}
}
if ($headerImage === '' && !$isMissing) {
$headerImage = '
<div class="panel-icon" id="' . $objectId . '_iconcontainer">
' . $this->iconFactory
->getIconForRecord(self::FILE_REFERENCE_TABLE, $databaseRow, IconSize::SMALL)
->setTitle($altText)
->render() . '
</div>';
}
return '
<button class="panel-button' . ($isExpanded ? '' : ' collapsed') . '" type="button"
data-bs-toggle="collapse" data-bs-target="#' . $ariaControls . '"
aria-expanded="' . ($isExpanded ? 'true' : 'false') . '" aria-controls="' . $ariaControls . '">
<span class="caret"></span>
' . $headerImage . '
<div class="panel-title"><span id="' . $objectId . '_label">' . $recordTitle . '</span></div>
' . $headerBadge . '
</button>
<div class="panel-actions t3js-formengine-irre-control">
' . $this->renderFileReferenceHeaderControl() . '
</div>';
}
/**
* Render the control-icons for a file reference (e.g. create new, sorting, delete, disable/enable).
*/
protected function renderFileReferenceHeaderControl(): string
{
$controls = [];
$databaseRow = $this->data['databaseRow'];
$databaseRow += [
'uid' => 0,
];
$parentConfig = $this->data['inlineParentConfig'];
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUserAuthentication();
$isNewItem = str_starts_with((string)$databaseRow['uid'], 'NEW');
$fileReferenceTableTca = $this->data['tcaSchemata']->get(self::FILE_REFERENCE_TABLE);
$calcPerms = new Permission(
$backendUser->calcPerms(BackendUtility::readPageAccess(
(int)($this->data['parentPageRow']['uid'] ?? 0),
$backendUser->getPagePermsClause(Permission::PAGE_SHOW)
))
);
$event = $this->eventDispatcher->dispatch(
new ModifyFileReferenceEnabledControlsEvent($this->data, $databaseRow)
);
if ($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$controls['localize'] = $this->iconFactory
->getIcon('actions-edit-localize-status-low', IconSize::SMALL)
->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize.isLocalizable'))
->render();
}
if ($event->isControlEnabled('info')) {
if ($isNewItem) {
$controls['info'] = '
<span class="btn btn-default disabled">
' . $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render() . '
</span>';
} else {
$controls['info'] = '
<button type="button" class="btn btn-default" data-action="infowindow" data-info-table="' . htmlspecialchars('_FILE') . '" data-info-uid="' . (int)$databaseRow['uid_local'][0]['uid'] . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')) . '">
' . $this->iconFactory->getIcon('actions-document-info', IconSize::SMALL)->render() . '
</button>';
}
}
// If the table is NOT a read-only table, then show these links:
if (!($parentConfig['readOnly'] ?? false)
&& !($fileReferenceTableTca->getCapability(TcaSchemaCapability::AccessReadOnly)->getValue() ?? false)
&& !($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)
) {
if ($event->isControlEnabled('sort')) {
$icon = 'actions-move-up';
$class = '';
if ((int)$parentConfig['inline']['first'] === (int)$databaseRow['uid']) {
$class = ' disabled';
$icon = 'empty-empty';
}
$controls['sort.up'] = '
<button type="button" class="btn btn-default' . $class . '" data-action="sort" data-direction="up" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveUp')) . '">
' . $this->iconFactory->getIcon($icon, IconSize::SMALL)->render() . '
</button>';
$icon = 'actions-move-down';
$class = '';
if ((int)$parentConfig['inline']['last'] === (int)$databaseRow['uid']) {
$class = ' disabled';
$icon = 'empty-empty';
}
$controls['sort.down'] = '
<button type="button" class="btn btn-default' . $class . '" data-action="sort" data-direction="down" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveDown')) . '">
' . $this->iconFactory->getIcon($icon, IconSize::SMALL)->render() . '
</button>';
}
$sysFileMetadataTableTca = $this->data['tcaSchemata']->has('sys_file_metadata') ? $this->data['tcaSchemata']->get('sys_file_metadata') : null;
if (!$isNewItem
&& ($languageField = ($sysFileMetadataTableTca?->getRawConfiguration()['languageField'] ?? false))
&& $backendUser->check('tables_modify', 'sys_file_metadata')
&& $event->isControlEnabled('edit')
) {
$languageId = (int)(is_array($databaseRow[$languageField] ?? null)
? ($databaseRow[$languageField][0] ?? 0)
: ($databaseRow[$languageField] ?? 0));
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_file_metadata');
$metadataRecord = $queryBuilder
->select('uid')
->from('sys_file_metadata')
->where(
$queryBuilder->expr()->eq(
'file',
$queryBuilder->createNamedParameter((int)$databaseRow['uid_local'][0]['uid'], Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
$languageField,
$queryBuilder->createNamedParameter($languageId, Connection::PARAM_INT)
)
)
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!empty($metadataRecord)) {
$url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
'edit[sys_file_metadata][' . (int)$metadataRecord['uid'] . ']' => 'edit',
'module' => (string)($this->data['request']->getQueryParams()['module'] ?? ''),
'returnUrl' => $this->data['returnUrl'],
]);
$controls['edit'] = '
<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata')) . '">
' . $this->iconFactory->getIcon('actions-open', IconSize::SMALL)->render() . '
</a>';
}
}
if ($event->isControlEnabled('delete') && $calcPerms->editContentPermissionIsGranted()) {
$recordInfo = $this->data['databaseRow']['uid_local'][0]['title'] ?? $this->data['recordTitle'] ?? '';
if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) {
$recordInfo .= ' [' . $this->data['tableName'] . ':' . $this->data['vanillaUid'] . ']';
}
$controls['delete'] = '
<button type="button" class="btn btn-default t3js-editform-delete-inline-record" data-record-info="' . htmlspecialchars(trim($recordInfo)) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete')) . '">
' . $this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL)->render() . '
</button>';
}
if (($hiddenField = ($fileReferenceTableTca->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName())) !== ''
&& ($fileReferenceTableTca->hasField($hiddenField))
&& $event->isControlEnabled('hide')
&& (
!($fileReferenceTableTca->getField($hiddenField)->getConfiguration()['exclude'] ?? false)
|| $backendUser->check('non_exclude_fields', self::FILE_REFERENCE_TABLE . ':' . $hiddenField)
)
) {
if ($databaseRow[$hiddenField] ?? false) {
$controls['hide'] = '
<button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide')) . '">
' . $this->iconFactory->getIcon('actions-edit-unhide', IconSize::SMALL)->render() . '
</button>';
} else {
$controls['hide'] = '
<button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide')) . '">
' . $this->iconFactory->getIcon('actions-edit-hide', IconSize::SMALL)->render() . '
</button>';
}
}
if (($parentConfig['appearance']['useSortable'] ?? false) && $event->isControlEnabled('dragdrop')) {
$controls['dragdrop'] = '
<span class="btn btn-default sortableHandle" data-id="' . (int)$databaseRow['uid'] . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move')) . '">
' . $this->iconFactory->getIcon('actions-move-move', IconSize::SMALL)->render() . '
</span>';
}
} elseif (($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)
&& MathUtility::canBeInterpretedAsInteger($this->data['inlineParentUid'])
&& $event->isControlEnabled('localize')
) {
$controls['localize'] = '
<button type="button" class="btn btn-default t3js-synchronizelocalize-button" data-type="' . htmlspecialchars((string)$databaseRow['uid']) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize')) . '">
' . $this->iconFactory->getIcon('actions-document-localize', IconSize::SMALL)->render() . '
</button>';
}
if ($lockInfo = BackendUtility::isRecordLocked(self::FILE_REFERENCE_TABLE, $databaseRow['uid'])) {
$controls['locked'] = '
<button type="button" class="btn btn-default" title="' . htmlspecialchars($lockInfo['msg']) . '">
' . $this->iconFactory->getIcon('status-user-backend', IconSize::SMALL, 'overlay-edit')->render() . '
</button>';
}
// Get modified controls. This means their markup was modified, new controls were added or controls got removed.
$controls = $this->eventDispatcher->dispatch(
new ModifyFileReferenceControlsEvent($controls, $this->data, $databaseRow)
)->getControls();
$out = '';
if (($controls['edit'] ?? false) || ($controls['hide'] ?? false) || ($controls['delete'] ?? false)) {
$out .= '
<div class="btn-group btn-group-sm" role="group">
' . ($controls['edit'] ?? '') . ($controls['hide'] ?? '') . ($controls['delete'] ?? '') . '
</div>';
unset($controls['edit'], $controls['hide'], $controls['delete']);
}
if (($controls['info'] ?? false) || ($controls['new'] ?? false) || ($controls['sort.up'] ?? false) || ($controls['sort.down'] ?? false) || ($controls['dragdrop'] ?? false)) {
$out .= '
<div class="btn-group btn-group-sm" role="group">
' . ($controls['info'] ?? '') . ($controls['new'] ?? '') . ($controls['sort.up'] ?? '') . ($controls['sort.down'] ?? '') . ($controls['dragdrop'] ?? '') . '
</div>';
unset($controls['info'], $controls['new'], $controls['sort.up'], $controls['sort.down'], $controls['dragdrop']);
}
if ($controls['localize'] ?? false) {
$out .= '<div class="btn-group btn-group-sm" role="group">' . $controls['localize'] . '</div>';
unset($controls['localize']);
}
if ($controls !== [] && ($remainingControls = trim(implode('', $controls))) !== '') {
$out .= '<div class="btn-group btn-group-sm" role="group">' . $remainingControls . '</div>';
}
return $out;
}
protected function getRecordTitle(): string
{
$databaseRow = $this->data['databaseRow'];
$fileRecord = $databaseRow['uid_local'][0]['row'] ?? null;
if ($fileRecord === null) {
return $this->data['recordTitle'] ?: (string)$databaseRow['uid'];
}
$title = '<span>' . $this->getLabelFieldForRecord($databaseRow, $fileRecord, 'name') . '</span>';
// In debug mode, add the table name to the record title
if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) {
$title .= ' <span class="panel-meta"><code>[' . self::FILE_REFERENCE_TABLE . ']</code></span>';
}
return $title;
}
protected function getLabelFieldForRecord(array $databaseRow, array $fileRecord, string $field): string
{
$value = '';
if (isset($databaseRow[$field])) {
$value = htmlspecialchars((string)$databaseRow[$field]);
} elseif (isset($fileRecord[$field])) {
$value = htmlspecialchars((string)$fileRecord[$field]);
}
return $value;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}