TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:24 +02:00
commit aad9daaefd
1506 changed files with 94005 additions and 0 deletions
@@ -0,0 +1,41 @@
<?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\Form\ViewHelpers\Be;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* Return the max file size for use in the form editor
*
* Scope: backend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-be-maximumfilesize
* @internal
*/
final class MaximumFileSizeViewHelper extends AbstractViewHelper
{
public function render(): string
{
$maxUploadFileSize = GeneralUtility::getMaxUploadFileSize();
// format according to PHP formatting rules (K = kilobytes instead of kibibytes)
$formattedSize = GeneralUtility::formatSize($maxUploadFileSize * 1024, '|k|M|G|T|P|E|Z|Y');
// remove decimals from result to match EXT:form validator integer format
return preg_replace('/(\d+)(.+\d{2})?([kMGTPEZY]{0,1})/', '$1$3', $formattedSize);
}
}
@@ -0,0 +1,145 @@
<?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\Form\ViewHelpers\Be;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Context\PageContext;
use TYPO3\CMS\Backend\Context\PageContextFactory;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumn;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
use TYPO3\CMS\Backend\View\Drawing\DrawingConfiguration;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Backend\View\PageViewMode;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* Used by the form editor.
* Render a content element preview like the page module
*
* Scope: backend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-be-rendercontentelementpreview
* @internal
*/
#[Autoconfigure(public: true)]
final class RenderContentElementPreviewViewHelper extends AbstractViewHelper
{
/**
* As this ViewHelper renders HTML, the output must not be escaped.
*
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly PageContextFactory $pageContextFactory,
private readonly UriBuilder $uriBuilder,
private readonly RecordFactory $recordFactory,
) {}
public function initializeArguments(): void
{
$this->registerArgument('contentElementUid', 'int', 'The uid of a content element');
$this->registerArgument('formPersistenceIdentifier', 'string', 'The form persistence identifier for return URL', false, '');
}
public function render(): string
{
$content = '';
$contentElementUid = $this->arguments['contentElementUid'];
$contentRecord = BackendUtility::getRecord('tt_content', $contentElementUid);
$request = null;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
if (!empty($contentRecord) && $request !== null) {
$backendLayout = GeneralUtility::makeInstance(BackendLayout::class, 'dummy', 'dummy', []);
$pageId = (int)$contentRecord['pid'];
$pageContext = $request->getAttribute('pageContext');
if (!$pageContext instanceof PageContext) {
try {
$pageContext = $this->pageContextFactory->createFromRequest($request, $pageId, $this->getBackendUser());
} catch (\Exception $e) {
return '';
}
}
$manipulatedRequest = $this->getManipulatedRequestToFormEditor($request, $contentRecord);
$pageLayoutContext = GeneralUtility::makeInstance(
PageLayoutContext::class,
$pageContext,
$backendLayout,
DrawingConfiguration::create($backendLayout, BackendUtility::getPagesTSconfig($pageId), PageViewMode::LayoutView),
$manipulatedRequest
);
$gridColumn = GeneralUtility::makeInstance(GridColumn::class, $pageLayoutContext, []);
$contentRecord = $this->recordFactory->createResolvedRecordFromDatabaseRow('tt_content', $contentRecord, null, $pageLayoutContext->getRecordIdentityMap());
$columnItem = GeneralUtility::makeInstance(GridColumnItem::class, $pageLayoutContext, $gridColumn, $contentRecord);
return $columnItem->getPreview();
}
return $content;
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
/**
* Create a manipulated request with custom NormalizedParams to override the return URL.
* This allows customizing the return URL used by PageLayoutContext->getReturnUrl()
* without modifying the original request or the PageLayoutContext logic.
*/
private function getManipulatedRequestToFormEditor(ServerRequestInterface $request, array $contentRecord): ServerRequestInterface
{
$serverParams = $request->getServerParams();
$serverParams['REQUEST_URI'] = $this->buildCustomReturnUrl($request, $contentRecord);
$customNormalizedParams = NormalizedParams::createFromServerParams($serverParams);
return $request->withAttribute('normalizedParams', $customNormalizedParams);
}
/**
* Build the custom return URL for the form editor.
* Generates the URL to FormEditor->index action with the formPersistenceIdentifier parameter.
*/
private function buildCustomReturnUrl(ServerRequestInterface $request, array $contentRecord): string
{
$formPersistenceIdentifier = $this->arguments['formPersistenceIdentifier'] ?? '';
if (empty($formPersistenceIdentifier)) {
return $request->getAttribute('normalizedParams')->getRequestUri();
}
$uri = $this->uriBuilder->buildUriFromRoute(
'form_editor',
['formPersistenceIdentifier' => $formPersistenceIdentifier]
);
return (string)$uri;
}
}
@@ -0,0 +1,117 @@
<?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\Form\ViewHelpers\Form;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper;
use TYPO3\CMS\Form\Security\HashScope;
/**
* ViewHelper which renders a checkbox for file deletion in EXT:form.
*
* This ViewHelper is similar to Extbase's UploadDeleteCheckboxViewHelper but adapted
* for the EXT:form context. It renders a checkbox that, when checked, marks the
* associated file for deletion on form submission.
*
* Example usage:
* ```
* <formvh:form.uploadDeleteCheckbox
* property="{element.identifier}"
* fileReference="{file}"
* fileIndex="{iterator.index}"
* />
* ```
*
* Scope: frontend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-form-uploaddeletecheckbox
*/
final class UploadDeleteCheckboxViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function __construct(
private readonly HashService $hashService,
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('fileReference', FileReference::class, 'The file reference object', true);
$this->registerArgument('fileIndex', 'int', 'Index of the file in multiple upload context', false, 0);
}
public function render(): string
{
/** @var FileReference|null $fileReference */
$fileReference = $this->arguments['fileReference'];
$fileIndex = (int)$this->arguments['fileIndex'];
// Early return if no file reference given
if (!$fileReference instanceof FileReference) {
return '';
}
$this->tag->addAttribute('type', 'checkbox');
// Build the deletion data that will be validated on submit
$deleteData = [
'property' => $this->arguments['property'],
'fileIndex' => $fileIndex,
'fileUid' => $fileReference->getUid() ?? $fileReference->getOriginalResource()->getOriginalFile()->getUid(),
];
// Create HMAC-signed value
$valueAttribute = $this->hashService->appendHmac(
json_encode($deleteData, JSON_THROW_ON_ERROR),
HashScope::DeleteFile->prefix()
);
// Build name attribute using the form field prefix
$name = $this->getName();
$nameAttribute = $name . '[__deleteFile][' . $fileIndex . ']';
$this->tag->addAttribute('name', $nameAttribute);
$this->tag->addAttribute('value', $valueAttribute);
// Check if this checkbox was previously checked (in case of validation errors)
if ($this->isChecked($fileIndex)) {
$this->tag->addAttribute('checked', 'checked');
}
return $this->tag->render();
}
/**
* Checks if the checkbox for the given file index was checked in the current request
*/
private function isChecked(int $fileIndex): bool
{
$value = $this->getValueAttribute();
if (is_array($value) && isset($value['__deleteFile'][$fileIndex])) {
return !empty($value['__deleteFile'][$fileIndex]);
}
return false;
}
}
@@ -0,0 +1,162 @@
<?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\Form\ViewHelpers\Form;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Property\PropertyMapper;
use TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper;
use TYPO3\CMS\Form\Security\HashScope;
/**
* This ViewHelper makes the specified Image object available for its
* childNodes.
* In case the form is redisplayed because of validation errors, a previously
* uploaded image will be correctly used.
*
* Scope: frontend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-form-uploadedresource
*/
final class UploadedResourceViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function __construct(
private readonly HashService $hashService,
private readonly PropertyMapper $propertyMapper,
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('as', 'string', '');
$this->registerArgument('accept', 'array', 'Values for the accept attribute', false, []);
$this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
$this->registerArgument('multiple', 'boolean', 'Defines the upload element accepting multiple files', false, false);
}
public function render(): string
{
$output = '';
$name = $this->getName();
$as = $this->arguments['as'];
$accept = $this->arguments['accept'];
$multiple = $this->arguments['multiple'];
$resource = $this->getUploadedResource();
if (!empty($accept)) {
$this->tag->addAttribute('accept', implode(',', $accept));
}
if ($resource !== null) {
if ($resource instanceof FileReference) {
$resourcePointerValue = $resource->getUid() ?? ('file:' . $resource->getOriginalResource()->getOriginalFile()->getUid());
$output .= $this->buildResourcePointerInput(
0,
(string)$resourcePointerValue,
$this->buildResourcePointerIdAttribute(),
);
} elseif ($resource instanceof ObjectStorage) {
foreach ($resource as $file) {
$index = $resource->getPosition($file);
$resourcePointerValue = $file->getUid() ?? ('file:' . $file->getOriginalResource()->getOriginalFile()->getUid());
$output .= $this->buildResourcePointerInput(
$index,
(string)$resourcePointerValue,
$this->buildResourcePointerIdAttribute('-' . $index),
);
}
}
$this->templateVariableContainer->add($as, $resource);
$output .= $this->renderChildren();
$this->templateVariableContainer->remove($as);
}
foreach (['name', 'type', 'tmp_name', 'error', 'size'] as $fieldName) {
$this->registerFieldNameForFormTokenGeneration($name . '[' . $fieldName . ']');
}
$this->tag->addAttribute('type', 'file');
if ($multiple === true) {
$this->tag->addAttribute('name', $name . '[]');
$this->tag->addAttribute('multiple', true);
} else {
$this->tag->addAttribute('name', $name);
}
$this->setErrorClassAttribute();
$output .= $this->tag->render();
return $output;
}
private function buildResourcePointerInput(int $index, string $resourcePointerValue, string $idAttribute): string
{
$name = htmlspecialchars($this->getName());
$hmac = htmlspecialchars($this->hashService->appendHmac($resourcePointerValue, HashScope::ResourcePointer->prefix()));
return '<input type="hidden"'
. ' name="' . $name . '[__submittedFiles][' . $index . '][submittedFile][resourcePointer]"'
. ' value="' . $hmac . '"'
. $idAttribute
. ' />';
}
private function buildResourcePointerIdAttribute(string $suffix = ''): string
{
if (!isset($this->additionalArguments['id'])) {
return '';
}
return ' id="' . htmlspecialchars($this->additionalArguments['id']) . '-file-reference' . $suffix . '"';
}
/**
* Return a previously uploaded resource.
* Return NULL if errors occurred during property mapping for this property.
*/
private function getUploadedResource(): FileReference|ObjectStorage|null
{
if ($this->getMappingResultsForProperty()->hasErrors()) {
return null;
}
$resource = $this->getValueAttribute();
if ($resource instanceof ObjectStorage) {
return $resource;
}
if ($resource instanceof FileReference) {
// When multiple uploads are enabled but the stored value is a single
// FileReference, wrap it in an ObjectStorage so that the Fluid template's
// f:for ViewHelper receives an iterable instead of crashing.
if ($this->arguments['multiple']) {
$storage = new ObjectStorage();
$storage->attach($resource);
return $storage;
}
return $resource;
}
return $this->propertyMapper->convert($resource, FileReference::class);
}
}
+92
View File
@@ -0,0 +1,92 @@
<?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!
*/
/*
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
*/
namespace TYPO3\CMS\Form\ViewHelpers;
use TYPO3\CMS\Core\Crypto\HashAlgo;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper as FluidFormViewHelper;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Security\HashScope;
use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;
/**
* Custom form ViewHelper that renders the form state instead of referrer fields
*
* Scope: frontend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-form
*/
final class FormViewHelper extends FluidFormViewHelper
{
/**
* Renders hidden form fields for referrer information about
* the current request.
*
* @return string Hidden fields with referrer information
*/
protected function renderHiddenReferrerFields(): string
{
$formRuntime = $this->getFormRuntime();
$prefix = $this->prefixFieldName($this->getFormObjectName());
$markup = $this->createHiddenInputElement(
$prefix . '[__state]',
$this->hashService->appendHmac(
base64_encode(serialize($formRuntime->getFormState())),
HashScope::FormState->prefix(),
HashAlgo::SHA3_256
)
);
// ONLY assign `__session` if form is performing (uncached)
if ($formRuntime->canProcessFormSubmission() && $formRuntime->getFormSession() !== null) {
$markup .= $this->createHiddenInputElement(
$prefix . '[__session]',
$formRuntime->getFormSession()->getAuthenticatedIdentifier()
);
}
return $markup;
}
private function createHiddenInputElement(string $name, string $value): string
{
$tagBuilder = GeneralUtility::makeInstance(TagBuilder::class, 'input');
$tagBuilder->addAttribute('type', 'hidden');
$tagBuilder->addAttribute('name', $name);
$tagBuilder->addAttribute('value', $value);
return $tagBuilder->render();
}
/**
* We do NOT return NULL as in this case, the Form ViewHelpers do not enter $objectAccessorMode.
* However, we return the form identifier.
*/
protected function getFormObjectName(): string
{
return $this->getFormRuntime()->getFormDefinition()->getIdentifier();
}
private function getFormRuntime(): FormRuntime
{
return $this->arguments['object'];
}
}
@@ -0,0 +1,110 @@
<?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\Form\ViewHelpers;
use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* Scope: frontend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-gridcolumnclassautoconfiguration
*/
final class GridColumnClassAutoConfigurationViewHelper extends AbstractViewHelper
{
/**
* @var bool
*/
protected $escapeOutput = false;
public function initializeArguments(): void
{
$this->registerArgument('element', RootRenderableInterface::class, 'A RootRenderableInterface instance', true);
}
public function render(): string
{
$formElement = $this->arguments['element'];
if ($formElement instanceof RenderableInterface && !$formElement->isEnabled()) {
return '';
}
$gridRowElement = $formElement->getParentRenderable();
$gridRowChildElements = $gridRowElement->getElements();
$gridViewPortConfiguration = $gridRowElement->getProperties()['gridColumnClassAutoConfiguration'];
if (empty($gridViewPortConfiguration)) {
return '';
}
$gridSize = (int)$gridViewPortConfiguration['gridSize'];
$columnsToCalculate = [];
$usedColumns = [];
foreach ($gridRowChildElements as $childElement) {
if ($childElement instanceof RenderableInterface && !$childElement->isEnabled()) {
continue;
}
if (empty($childElement->getProperties()['gridColumnClassAutoConfiguration'])) {
foreach ($gridViewPortConfiguration['viewPorts'] as $viewPortName => $configuration) {
$columnsToCalculate[$viewPortName]['elements'] = ($columnsToCalculate[$viewPortName]['elements'] ?? 0) + 1;
}
} else {
$gridColumnViewPortConfiguration = $childElement->getProperties()['gridColumnClassAutoConfiguration'];
foreach ($gridViewPortConfiguration['viewPorts'] as $viewPortName => $configuration) {
$configuration = $gridColumnViewPortConfiguration['viewPorts'][$viewPortName] ?? [];
if (
isset($configuration['numbersOfColumnsToUse'])
&& (int)$configuration['numbersOfColumnsToUse'] > 0
) {
$usedColumns[$viewPortName]['sum'] = ($usedColumns[$viewPortName]['sum'] ?? 0);
$usedColumns[$viewPortName]['sum'] += (int)$configuration['numbersOfColumnsToUse'];
if ($childElement->getIdentifier() === $formElement->getIdentifier()) {
$usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse'] = (int)$configuration['numbersOfColumnsToUse'];
if ($usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse'] > $gridSize) {
$usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse'] = $gridSize;
}
}
} else {
$columnsToCalculate[$viewPortName]['elements'] = ($columnsToCalculate[$viewPortName]['elements'] ?? 0) + 1;
}
}
}
}
$classes = [];
foreach ($gridViewPortConfiguration['viewPorts'] as $viewPortName => $configuration) {
if (isset($usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse'])) {
$numbersOfColumnsToUse = $usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse'];
} else {
$restColumnsToDivide = $gridSize - ($usedColumns[$viewPortName]['sum'] ?? 0);
$restElements = (int)$columnsToCalculate[$viewPortName]['elements'];
if ($restColumnsToDivide < 1) {
$restColumnsToDivide = $gridSize;
}
$numbersOfColumnsToUse = floor($restColumnsToDivide / $restElements);
}
$classes[] = str_replace(
'{@numbersOfColumnsToUse}',
(string)$numbersOfColumnsToUse,
$configuration['classPattern']
);
}
return implode(' ', $classes);
}
}
@@ -0,0 +1,74 @@
<?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!
*/
/*
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
*/
namespace TYPO3\CMS\Form\ViewHelpers;
use TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface;
use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* Renders the values of a form
*
* Scope: frontend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-renderallformvalues
*/
final class RenderAllFormValuesViewHelper extends AbstractViewHelper
{
/**
* @var bool
*/
protected $escapeOutput = false;
public function initializeArguments(): void
{
$this->registerArgument('renderable', RootRenderableInterface::class, 'A RootRenderableInterface instance', true);
$this->registerArgument('as', 'string', 'The name within the template', false, 'formValue');
}
/**
* Return array element by key.
*/
public function render(): string
{
$renderable = $this->arguments['renderable'];
if ($renderable instanceof CompositeRenderableInterface) {
$elements = $renderable->getRenderablesRecursively();
} else {
$elements = [$renderable];
}
$as = $this->arguments['as'];
$output = '';
foreach ($elements as $element) {
$output .= $this->renderingContext->getViewHelperInvoker()->invoke(
RenderFormValueViewHelper::class,
[
'renderable' => $element,
'as' => $as,
],
$this->renderingContext,
$this->renderChildren(...),
);
}
return $output;
}
}
@@ -0,0 +1,207 @@
<?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!
*/
/*
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
*/
namespace TYPO3\CMS\Form\ViewHelpers;
use TYPO3\CMS\Core\Country\CountryProvider;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
use TYPO3\CMS\Form\Domain\Model\FormElements\StringableFormElementInterface;
use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
use TYPO3Fluid\Fluid\Core\Variables\ScopedVariableProvider;
use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* Renders a single value of a form
*
* Scope: frontend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-renderformvalue
*/
final class RenderFormValueViewHelper extends AbstractViewHelper
{
/**
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly CountryProvider $countryProvider
) {}
public function initializeArguments(): void
{
$this->registerArgument('renderable', RenderableInterface::class, 'A renderable element', true);
$this->registerArgument('as', 'string', 'The name within the template', false, 'formValue');
}
/**
* Return array element by key
*/
public function render(): string
{
$element = $this->arguments['renderable'];
if (!$element instanceof FormElementInterface || !self::isEnabled($element)) {
return '';
}
$renderingOptions = $element->getRenderingOptions();
if ($renderingOptions['_isSection'] ?? false) {
$data = [
'element' => $element,
'isSection' => true,
];
} elseif ($renderingOptions['_isCompositeFormElement'] ?? false) {
return '';
} else {
$formRuntime = $this->renderingContext
->getViewHelperVariableContainer()
->get(RenderRenderableViewHelper::class, 'formRuntime');
$value = $formRuntime[$element->getIdentifier()];
$data = [
'element' => $element,
'value' => $value,
'processedValue' => $this->processElementValue($element, $value),
'isMultiValue' => is_iterable($value),
];
}
$variableProvider = new ScopedVariableProvider($this->renderingContext->getVariableProvider(), new StandardVariableProvider([$this->arguments['as'] => $data]));
$this->renderingContext->setVariableProvider($variableProvider);
$output = (string)$this->renderChildren();
$this->renderingContext->setVariableProvider($variableProvider->getGlobalVariableProvider());
return $output;
}
/**
* Converts the given value to a simple type (string or array) considering the underlying FormElement definition.
*
* @param mixed $value
* @return mixed
*/
private function processElementValue(
FormElementInterface $element,
$value
) {
$properties = $element->getProperties();
$options = $properties['options'] ?? null;
if ($element->getType() === 'CountrySelect') {
$country = $this->countryProvider->getByIsoCode($value ?? '');
if ($country !== null) {
return (string)LocalizationUtility::translate($country->getLocalizedNameLabel());
}
}
if (is_array($options)) {
$options = (array)$this->renderingContext->getViewHelperInvoker()->invoke(
TranslateElementPropertyViewHelper::class,
['element' => $element, 'property' => 'options'],
$this->renderingContext,
$this->renderChildren(...),
);
if (is_array($value)) {
return self::mapValuesToOptions($value, $options);
}
return self::mapValueToOption($value, $options);
}
if ($value instanceof ObjectStorage) {
$result = [];
foreach ($value as $item) {
$result[] = is_object($item) ? self::processObject($element, $item) : $item;
}
return $result;
}
if (is_object($value)) {
return self::processObject($element, $value);
}
return $value;
}
/**
* Replaces the given values (=keys) with the corresponding elements in $options.
*
* @see mapValueToOption()
*/
private static function mapValuesToOptions(array $value, array $options): array
{
$result = [];
foreach ($value as $key) {
$result[] = self::mapValueToOption($key, $options);
}
return $result;
}
/**
* Replaces the given value (=key) with the corresponding element in $options
* If the key does not exist in $options, it is returned without modification
*
* @param mixed $value
* @return mixed
*/
private static function mapValueToOption($value, array $options)
{
return $options[$value] ?? $value;
}
/**
* Converts the given $object to a string representation considering the $element FormElement definition.
*
* @param object $object
*/
private static function processObject(FormElementInterface $element, $object): string
{
if ($element instanceof StringableFormElementInterface) {
return $element->valueToString($object);
}
if ($object instanceof \DateTime) {
return $object->format(\DateTimeInterface::W3C);
}
if ($object instanceof File || $object instanceof FileReference) {
if ($object instanceof FileReference) {
$object = $object->getOriginalResource();
}
return $object->getName();
}
if (method_exists($object, '__toString')) {
return (string)$object;
}
return 'Object [' . get_class($object) . ']';
}
private static function isEnabled(RenderableInterface $renderable): bool
{
if (!$renderable->isEnabled()) {
return false;
}
while ($renderable = $renderable->getParentRenderable()) {
if (!$renderable->isEnabled()) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,84 @@
<?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!
*/
/*
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
*/
namespace TYPO3\CMS\Form\ViewHelpers;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Event\BeforeRenderableIsRenderedEvent;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* Render a renderable.
*
* Set the renderable into the \TYPO3\CMS\Form\Mvc\View\FormView
* and return the rendered content.
*
* Scope: frontend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-renderrenderable
*/
final class RenderRenderableViewHelper extends AbstractViewHelper
{
/**
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
) {}
public function initializeArguments(): void
{
$this->registerArgument('renderable', RootRenderableInterface::class, 'A RenderableInterface instance', true);
}
public function render(): string
{
/** @var FormRuntime $formRuntime */
$formRuntime = $this->renderingContext
->getViewHelperVariableContainer()
->get(self::class, 'formRuntime');
$renderable = $this->arguments['renderable'];
$this->eventDispatcher->dispatch(new BeforeRenderableIsRenderedEvent($renderable, $formRuntime));
$content = '';
if ($renderable instanceof FormRuntime || ($renderable instanceof RenderableInterface && $renderable->isEnabled())) {
$content = $this->renderChildren();
}
// Wrap every renderable with a span with an identifier path data attribute if previewMode is active
if (!empty($content)) {
$renderingOptions = $formRuntime->getRenderingOptions();
if (isset($renderingOptions['previewMode']) && $renderingOptions['previewMode'] === true) {
$path = $renderable->getIdentifier();
if ($renderable instanceof RenderableInterface) {
while ($renderable = $renderable->getParentRenderable()) {
$path = $renderable->getIdentifier() . '/' . $path;
}
}
$content = '<span data-element-identifier-path="' . $path . '">' . $content . '</span>';
}
}
return $content;
}
}
+104
View File
@@ -0,0 +1,104 @@
<?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!
*/
/*
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
*/
namespace TYPO3\CMS\Form\ViewHelpers;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory;
use TYPO3\CMS\Form\Domain\Factory\FormFactoryInterface;
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* Main Entry Point to render a Form into a Fluid Template
*
* Usage
* =====
*
* Default::
*
* {namespace formvh=TYPO3\CMS\Form\ViewHelpers}
* <formvh:render factoryClass="NameOfYourCustomFactoryClass" />
*
* The factory class must implement :php:`TYPO3\CMS\Form\Domain\Factory\FormFactoryInterface`.
*
* Scope: frontend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-render
*/
final class RenderViewHelper extends AbstractViewHelper
{
/**
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly FormPersistenceManagerInterface $formPersistenceManager,
private readonly ExtbaseConfigurationManagerInterface $extbaseConfigurationManager,
) {}
public function initializeArguments(): void
{
$this->registerArgument('persistenceIdentifier', 'string', 'The persistence identifier for the form.');
$this->registerArgument('factoryClass', 'string', 'The fully qualified class name of the factory', false, ArrayFormFactory::class);
$this->registerArgument('prototypeName', 'string', 'Name of the prototype to use');
$this->registerArgument('overrideConfiguration', 'array', 'factory specific configuration', false, []);
}
public function render(): ?string
{
$persistenceIdentifier = $this->arguments['persistenceIdentifier'];
$prototypeName = $this->arguments['prototypeName'];
$overrideConfiguration = $this->arguments['overrideConfiguration'];
/** @var RequestInterface $request */
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
// @todo: formvh:render() does not make sense without a persistenceIdentifier, does it?
if (!empty($persistenceIdentifier)) {
// The ConfigurationManager of ext:form needs ext:extbase ConfigurationManager to retrieve basic TS
// settings. ConfigurationManager of extbase should *usually* only be called in extbase context and
// needs a Request, which is usually set by extbase bootstrap.
// We are however (most likely) not in extbase context here.
// To prevent a fallback of extbase ConfigurationManager to $GLOBALS['TYPO3_REQUEST'], we set
// the request explicitly here, to then fetch $formSettings from ext:form ConfigurationManager.
// $typoScriptSettings is hand over to load() to apply TS overrides for single forms, see #92408.
$this->extbaseConfigurationManager->setRequest($request);
$typoScriptSettings = $this->extbaseConfigurationManager->getConfiguration(ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form');
$formConfiguration = $this->formPersistenceManager->load($persistenceIdentifier, $typoScriptSettings, $request);
ArrayUtility::mergeRecursiveWithOverrule($formConfiguration, $overrideConfiguration);
$overrideConfiguration = $formConfiguration;
$overrideConfiguration['persistenceIdentifier'] = $persistenceIdentifier;
}
if (empty($prototypeName)) {
$prototypeName = $overrideConfiguration['prototypeName'] ?? 'standard';
}
// Even though getContainer() is internal, we can't get container injected here due to static scope
/** @var FormFactoryInterface $factory */
$factory = GeneralUtility::getContainer()->get($this->arguments['factoryClass']);
$formDefinition = $factory->build($overrideConfiguration, $prototypeName, $request);
$form = $formDefinition->bind($request);
return $form->render();
}
}
@@ -0,0 +1,61 @@
<?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\Form\ViewHelpers;
use TYPO3\CMS\Extbase\Error\Error;
use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Service\TranslationService;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* Translate form element properties.
*
* Scope: frontend / backend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-translateelementerror
*/
final class TranslateElementErrorViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly TranslationService $translationService
) {}
public function initializeArguments(): void
{
$this->registerArgument('element', RootRenderableInterface::class, 'Form Element to translate', true);
$this->registerArgument('error', Error::class, 'Error', true);
}
public function render(): string
{
$element = $this->arguments['element'];
$error = $this->arguments['error'];
/** @var FormRuntime $formRuntime */
$formRuntime = $this->renderingContext
->getViewHelperVariableContainer()
->get(RenderRenderableViewHelper::class, 'formRuntime');
return $this->translationService->translateFormElementError(
$element,
$error->getCode(),
$error->getArguments(),
$error->__toString(),
$formRuntime
);
}
}
@@ -0,0 +1,93 @@
<?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\Form\ViewHelpers;
use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Service\TranslationService;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* Translate form element properties.
*
* Scope: frontend / backend
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-form-translateelementproperty
*/
final class TranslateElementPropertyViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly TranslationService $translationService
) {}
public function initializeArguments(): void
{
$this->registerArgument('element', RootRenderableInterface::class, 'Form Element to translate', true);
$this->registerArgument('property', 'mixed', 'Property to translate');
$this->registerArgument('renderingOptionProperty', 'mixed', 'Property to translate');
$this->registerArgument('languageKey', 'string', 'Language key ("da" for example) or "default" to use. Also a Locale object is possible. If empty, use current locale from the request.');
}
/**
* Return array element by key.
*/
public function render(): array|string|null
{
self::assertArgumentTypes($this->arguments);
$element = $this->arguments['element'];
$property = null;
if (!empty($this->arguments['property'])) {
$property = $this->arguments['property'];
} elseif (!empty($this->arguments['renderingOptionProperty'])) {
$property = $this->arguments['renderingOptionProperty'];
}
if (empty($property)) {
$propertyParts = [];
} elseif (is_array($property)) {
$propertyParts = $property;
} else {
$propertyParts = [$property];
}
/** @var FormRuntime $formRuntime */
$formRuntime = $this->renderingContext
->getViewHelperVariableContainer()
->get(RenderRenderableViewHelper::class, 'formRuntime');
return $this->translationService->translateFormElementValue($element, $propertyParts, $formRuntime, $this->arguments['languageKey']);
}
private static function assertArgumentTypes(array $arguments): void
{
foreach (['property', 'renderingOptionProperty'] as $argumentName) {
if (
!isset($arguments[$argumentName])
|| is_string($arguments[$argumentName])
|| is_array($arguments[$argumentName])
) {
continue;
}
throw new InvalidArgumentValueException(
sprintf(
'Arguments "%s" either must be string or array',
$argumentName
),
1504871830
);
}
}
}