TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:20 +02:00
commit c7a46689ff
115 changed files with 11736 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
<?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\Fluid\ViewHelpers\Asset;
use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;
/**
* ViewHelper to add CSS to the TYPO3 AssetCollector. Either a file or inline CSS can be added.
*
* ```
* <f:asset.css identifier="identifier123" href="EXT:my_ext/Resources/Public/Css/foo.css" inline="0" />
* <f:asset.css identifier="identifier123">
* .foo { color: black; }
* </f:asset.css>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-asset-css
*/
final class CssViewHelper extends AbstractTagBasedViewHelper
{
/**
* This VH does not produce direct output, thus does not need to be wrapped in an escaping node
*
* @var bool
*/
protected $escapeOutput = false;
/**
* Rendered children string is passed as CSS code,
* there is no point in HTML encoding anything from that.
*
* @var bool
*/
protected $escapeChildren = true;
public function __construct(
private readonly AssetCollector $assetCollector,
) {
parent::__construct();
}
public function initialize(): void
{
// Add a tag builder, that does not html encode values, because rendering with encoding happens in AssetRenderer
$this->setTagBuilder(
new class extends TagBuilder {
public function addAttribute($attributeName, $attributeValue, $escapeSpecialCharacters = false): void
{
parent::addAttribute($attributeName, $attributeValue, false);
}
}
);
parent::initialize();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('disabled', 'bool', 'Define whether or not the described stylesheet should be loaded and applied to the document.');
$this->registerArgument('csp', 'bool', 'Whether to collect a CSP hash value for this asset (default: true for external files, false for inline)', false, null);
$this->registerArgument('identifier', 'string', 'Use this identifier within templates to only inject your CSS once, even though it is added multiple times.', true);
$this->registerArgument('priority', 'boolean', 'Define whether the CSS should be included before other CSS. CSS will always be output in the <head> tag.', false, false);
$this->registerArgument('inline', 'bool', 'Define whether or not the referenced file should be loaded as inline styles (Only to be used if \'href\' is set).', false, false);
}
public function render(): string
{
$identifier = (string)$this->arguments['identifier'];
$attributes = $this->tag->getAttributes();
// boolean attributes shall output attr="attr" if set
if ($this->arguments['disabled'] ?? false) {
$attributes['disabled'] = 'disabled';
}
$file = $attributes['href'] ?? null;
unset($attributes['href']);
$isExternalFile = $file !== null && !($this->arguments['inline'] ?? false);
$useCsp = $this->resolveCspOption($isExternalFile);
$options = [
'priority' => $this->arguments['priority'],
'csp' => $useCsp,
];
if ($file !== null) {
if ($this->arguments['inline'] ?? false) {
$content = @file_get_contents(GeneralUtility::getFileAbsFileName(trim($file)));
if ($content !== false) {
$this->assetCollector->addInlineStyleSheet($identifier, $content, $attributes, $options);
}
} else {
$this->assetCollector->addStyleSheet($identifier, $file, $attributes, $options);
}
} else {
$content = (string)$this->renderChildren();
if ($content !== '') {
$this->assetCollector->addInlineStyleSheet($identifier, $content, $attributes, $options);
}
}
return '';
}
private function resolveCspOption(bool $defaultForStatic): bool
{
$csp = $this->arguments['csp'];
if ($csp !== null) {
return (bool)$csp;
}
// Default: true for external files (allows hash collection), false for inline
return $defaultForStatic;
}
}
@@ -0,0 +1,59 @@
<?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\Fluid\ViewHelpers\Asset;
use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to add JavaScript modules to the TYPO3 AssetCollector.
*
* Examples
* ========
*
* ::
*
* <f:asset.module identifier="@my/package/filename.js"/>
*
* Details
* =======
*
* In the AssetCollector, the "identifier" attribute is used as a unique identifier. Thus, if modules are added multiple
* times using the same module identifier, the asset will only be served once.
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-asset-module
*/
final class ModuleViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly AssetCollector $assetCollector,
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('identifier', 'string', 'Bare module identifier like "@my/package/filename.js".', true);
}
public function render(): string
{
$identifier = (string)$this->arguments['identifier'];
$this->assetCollector->addJavaScriptModule($identifier);
return '';
}
}
@@ -0,0 +1,133 @@
<?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\Fluid\ViewHelpers\Asset;
use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;
/**
* ViewHelper to add JavaScript to the TYPO3 AssetCollector. Either a file or inline JavaScript can be added.
*
* ```
* <f:asset.script identifier="identifier123" src="EXT:my_ext/Resources/Public/JavaScript/foo.js" inline="0" />
* <f:asset.script identifier="identifier123">
* alert('hello world');
* </f:asset.script>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-asset-script
*/
final class ScriptViewHelper extends AbstractTagBasedViewHelper
{
/**
* This VH does not produce direct output, thus does not need to be wrapped in an escaping node
*
* @var bool
*/
protected $escapeOutput = false;
/**
* Rendered children string is passed as JavaScript code,
* there is no point in HTML encoding anything from that.
*
* @var bool
*/
protected $escapeChildren = false;
public function __construct(
private readonly AssetCollector $assetCollector,
) {
parent::__construct();
}
public function initialize(): void
{
// Add a tag builder, that does not html encode values, because rendering with encoding happens in AssetRenderer
$this->setTagBuilder(
new class extends TagBuilder {
public function addAttribute($attributeName, $attributeValue, $escapeSpecialCharacters = false): void
{
parent::addAttribute($attributeName, $attributeValue, false);
}
}
);
parent::initialize();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('async', 'bool', 'Define that the script will be fetched in parallel to parsing and evaluation.');
$this->registerArgument('defer', 'bool', 'Define that the script is meant to be executed after the document has been parsed.');
$this->registerArgument('nomodule', 'bool', 'Define that the script should not be executed in browsers that support ES2015 modules.');
$this->registerArgument('csp', 'bool', 'Whether to collect a CSP hash value for this asset (default: true for external files, false for inline)', false, null);
$this->registerArgument('identifier', 'string', 'Use this identifier within templates to only inject your JS once, even though it is added multiple times.', true);
$this->registerArgument('priority', 'boolean', 'Define whether the JavaScript should be put in the <head> tag above-the-fold or somewhere in the body part.', false, false);
$this->registerArgument('inline', 'bool', 'Define whether or not the referenced file should be loaded as inline script (Only to be used if \'src\' is set).', false, false);
}
public function render(): string
{
$identifier = (string)$this->arguments['identifier'];
$attributes = $this->tag->getAttributes();
// boolean attributes shall output attr="attr" if set
foreach (['async', 'defer', 'nomodule'] as $attribute) {
if ($this->arguments[$attribute] ?? false) {
$attributes[$attribute] = $attribute;
}
}
$src = $attributes['src'] ?? null;
unset($attributes['src']);
$isExternalFile = $src !== null && !($this->arguments['inline'] ?? false);
$useCsp = $this->resolveCspOption($isExternalFile);
$options = [
'priority' => $this->arguments['priority'],
'csp' => $useCsp,
];
if ($src !== null) {
if ($this->arguments['inline'] ?? false) {
$content = @file_get_contents(GeneralUtility::getFileAbsFileName(trim($src)));
if ($content !== false) {
$this->assetCollector->addInlineJavaScript($identifier, $content, $attributes, $options);
}
} else {
$this->assetCollector->addJavaScript($identifier, $src, $attributes, $options);
}
} else {
$content = (string)$this->renderChildren();
if ($content !== '') {
$this->assetCollector->addInlineJavaScript($identifier, $content, $attributes, $options);
}
}
return '';
}
private function resolveCspOption(bool $defaultForStatic): bool
{
$csp = $this->arguments['csp'];
if ($csp !== null) {
return (bool)$csp;
}
// Default: true for external files (allows hash collection), false for inline
return $defaultForStatic;
}
}
@@ -0,0 +1,55 @@
<?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\Fluid\ViewHelpers\Asset;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper for inline style attributes that need CSP hash coverage via `style-src-attr`.
*
* When `csp` is true (default), the style value is hashed and registered with the
* HashCollection so that the CSP header includes the corresponding `sha256-...` hash.
*
* Usage:
* ```
* <div style="{f:asset.styleAttr(value: 'color: green; text-decoration: underline;', csp: true)}">...</div>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-asset-styleattr
*/
final class StyleAttrViewHelper extends AbstractViewHelper
{
public function __construct(private readonly DirectiveHashCollection $directiveHashCollection) {}
public function initializeArguments(): void
{
$this->registerArgument('value', 'string', 'The inline style value (e.g. "color: green; text-decoration: underline;")', true);
$this->registerArgument('csp', 'bool', 'Whether to collect a CSP hash for this style value', false, true);
}
public function render(): string
{
$value = trim($this->arguments['value'] ?? $this->renderChildren());
if ($this->arguments['csp'] ?? true) {
$this->directiveHashCollection->addInlineHash(Directive::StyleSrcAttr, $value);
}
return $value;
}
}
@@ -0,0 +1,72 @@
<?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\Fluid\ViewHelpers\Be;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* The abstract base class for all backend ViewHelpers
* Note: backend ViewHelpers are still experimental!
*
* @deprecated since TYPO3 v15.0, will be removed in TYPO3 v16.0.
*/
abstract class AbstractBackendViewHelper extends AbstractViewHelper
{
/**
* Gets instance of template if exists or create a new one.
* Saves instance in viewHelperVariableContainer
*/
public function getModuleTemplate(): ModuleTemplate
{
trigger_error(
'AbstractBackendViewHelper::getModuleTemplate() has been deprecated in TYPO3 v15.0 and will be removed in v16.0.',
E_USER_DEPRECATED
);
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if ($viewHelperVariableContainer->exists(self::class, 'ModuleTemplate')) {
$moduleTemplate = $viewHelperVariableContainer->get(self::class, 'ModuleTemplate');
} else {
$moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
$viewHelperVariableContainer->add(self::class, 'ModuleTemplate', $moduleTemplate);
}
return $moduleTemplate;
}
/**
* Gets instance of PageRenderer if exists or create a new one.
* Saves instance in viewHelperVariableContainer
*/
public function getPageRenderer(): PageRenderer
{
trigger_error(
'AbstractBackendViewHelper::getPageRenderer() has been deprecated in TYPO3 v15.0 and will be removed in v16.0.',
E_USER_DEPRECATED
);
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if ($viewHelperVariableContainer->exists(self::class, 'PageRenderer')) {
$pageRenderer = $viewHelperVariableContainer->get(self::class, 'PageRenderer');
} else {
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$viewHelperVariableContainer->add(self::class, 'PageRenderer', $pageRenderer);
}
return $pageRenderer;
}
}
@@ -0,0 +1,101 @@
<?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\Fluid\ViewHelpers\Be;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper for rendering a styled content infobox markup.
*
* ```
* <f:be.infobox title="Message title">your box content</f:be.infobox>
* <f:be.infobox title="Error!" state="{f:constant(name: 'TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR')}" iconName="check">your box content</f:be.infobox>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-infobox
*/
final class InfoboxViewHelper extends AbstractViewHelper
{
/**
* As this ViewHelper renders HTML, the output must not be escaped.
*
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly IconFactory $iconFactory
) {}
public function initializeArguments(): void
{
$this->registerArgument('message', 'string', 'The message of the info box, if NULL tag content is used');
$this->registerArgument('title', 'string', 'The title of the info box');
$this->registerArgument('state', 'mixed', 'The state of the box, accepts ContextualFeedbackSeverity enum or integer value', false, ContextualFeedbackSeverity::NOTICE);
$this->registerArgument('iconName', 'string', 'Identifier of the icon as registered in the Icon Registry. NULL sets default icon');
$this->registerArgument('disableIcon', 'bool', 'If set to TRUE, the icon is not rendered.', false, false);
}
public function render(): string
{
$title = (string)$this->arguments['title'];
$message = (string)$this->renderChildren();
$state = $this->arguments['state'];
// The state argument accepts both a ContextualFeedbackSeverity enum and a raw integer value
if ($state instanceof ContextualFeedbackSeverity) {
$severity = $state;
} else {
$state = (int)$state;
$severity = ContextualFeedbackSeverity::from($state);
}
$disableIcon = $this->arguments['disableIcon'];
$icon = $this->arguments['iconName'] ?? $severity->getIconIdentifier();
$iconTemplate = '';
if (!$disableIcon) {
$iconTemplate = ''
. '<div class="callout-icon">'
. '<span class="icon-emphasized">'
. $this->iconFactory->getIcon($icon, IconSize::SMALL)->render()
. '</span>'
. '</div>';
}
$titleTemplate = '';
if ($title !== '') {
$titleTemplate = '<div class="callout-title">' . htmlspecialchars($title) . '</div>';
}
return '<div class="callout callout-' . htmlspecialchars($severity->getCssClass()) . '">'
. $iconTemplate
. '<div class="callout-content">'
. $titleTemplate
. '<div class="callout-body">' . $message . '</div>'
. '</div>'
. '</div>';
}
/**
* Explicitly set argument name to be used as content.
*/
public function getContentArgumentName(): string
{
return 'message';
}
}
+64
View File
@@ -0,0 +1,64 @@
<?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\Fluid\ViewHelpers\Be;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper for creating URIs to backend modules.
*
* ```
* <f:be.link route="web_ts" parameters="{id: 92}">Go to web_ts</f:be.link>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-link
*/
final class LinkViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'a';
public function __construct(
private readonly UriBuilder $uriBuilder
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('route', 'string', 'The name of the route', true);
$this->registerArgument('parameters', 'array', 'An array of parameters', false, []);
$this->registerArgument('referenceType', 'string', 'The type of reference to be generated (one of the constants)', false, UriBuilder::ABSOLUTE_PATH);
}
public function render(): string
{
$route = $this->arguments['route'];
$parameters = $this->arguments['parameters'];
$referenceType = $this->arguments['referenceType'];
$uri = $this->uriBuilder->buildUriFromRoute($route, $parameters, $referenceType);
$this->tag->addAttribute('href', (string)$uri);
$this->tag->setContent((string)$this->renderChildren());
$this->tag->forceClosingTag(true);
return $this->tag->render();
}
}
@@ -0,0 +1,68 @@
<?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\Fluid\ViewHelpers\Be\Menus;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper which groups options within a `<f:be.menus.actionMenu>` group.
*
* ```
* <f:be.menus.actionMenu>
* <f:be.menus.actionMenuItem label="First Menu" controller="Default" action="index" />
* <f:be.menus.actionMenuItemGroup label="Information">
* <f:be.menus.actionMenuItem label="PHP Information" controller="Information" action="listPhpInfo" />
* <f:be.menus.actionMenuItem label="{f:translate(key:'documentation')}" controller="Information" action="documentation" />
* ...
* </f:be.menus.actionMenuItemGroup>
* </f:be.menus.actionMenu>
* ```
*
* **NOTE**: This ViewHelper is experimental and tailored to be used only in extbase context.
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-menus-actionmenuitemgroup
*/
final class ActionMenuItemGroupViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'optgroup';
public function initializeArguments(): void
{
parent::initializeArguments();
// @todo: deprecate
$this->registerArgument('defaultController', 'string', 'Unused');
$this->registerArgument('label', 'string', 'The label of the option group', false, '');
}
public function render(): string
{
$this->tag->addAttribute('label', $this->arguments['label']);
$options = '';
foreach ($this->viewHelperNode->getChildNodes() as $childNode) {
if ($childNode instanceof ViewHelperNode) {
$options .= $childNode->evaluate($this->renderingContext);
}
}
$this->tag->setContent($options);
return $this->tag->render();
}
}
@@ -0,0 +1,112 @@
<?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\Fluid\ViewHelpers\Be\Menus;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper which returns an option tag within a `<f:be.menus.actionMenu>` group.
*
* ```
* <f:be.menus.actionMenu>
* <f:be.menus.actionMenuItem label="First Menu" controller="Default" action="index" />
* <f:be.menus.actionMenuItemGroup label="Information">
* <f:be.menus.actionMenuItem label="PHP Information" controller="Information" action="listPhpInfo" />
* <f:be.menus.actionMenuItem label="{f:translate(key:'documentation')}" controller="Information" action="documentation" />
* ...
* </f:be.menus.actionMenuItemGroup>
* </f:be.menus.actionMenu>
* ```
*
* **Note:** This ViewHelper is experimental and tailored to be used only in extbase context.
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-menus-actionmenuitem
*/
final class ActionMenuItemViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'option';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('label', 'string', 'label of the option tag', true);
$this->registerArgument('controller', 'string', 'controller to be associated with this ActionMenuItem', true);
$this->registerArgument('action', 'string', 'the action to be associated with this ActionMenuItem', true);
$this->registerArgument('arguments', 'array', 'additional controller arguments to be passed to the action when this ActionMenuItem is selected', false, []);
}
public function render(): string
{
$label = $this->arguments['label'];
$controller = $this->arguments['controller'];
$action = $this->arguments['action'];
$arguments = $this->arguments['arguments'];
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)
|| !$this->renderingContext->getAttribute(ServerRequestInterface::class) instanceof RequestInterface) {
// Throw if not an extbase request
throw new \RuntimeException(
'ViewHelper f:be.menus.actionMenuItem needs an extbase Request object to create URIs.',
1639741792
);
}
/** @var RequestInterface $request */
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$uriBuilder->setRequest($request);
$uri = $uriBuilder->reset()->uriFor($action, $arguments, $controller);
$this->tag->addAttribute('value', $uri);
if (!$this->tag->hasAttribute('selected')) {
$this->evaluateSelectItemState($controller, $action, $arguments);
}
$this->tag->setContent(htmlspecialchars($label, ENT_QUOTES, '', true));
return $this->tag->render();
}
private function evaluateSelectItemState(string $controller, string $action, array $arguments): void
{
/** @var RequestInterface $request */
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$flatRequestArguments = ArrayUtility::flattenPlain(
array_merge([
'controller' => $request->getControllerName(),
'action' => $request->getControllerActionName(),
], $request->getArguments())
);
$flatViewHelperArguments = ArrayUtility::flattenPlain(
array_merge(['controller' => $controller, 'action' => $action], $arguments)
);
if (
($this->arguments['selected'] ?? false)
|| array_diff($flatRequestArguments, $flatViewHelperArguments) === []
) {
$this->tag->addAttribute('selected', 'selected');
}
}
}
@@ -0,0 +1,91 @@
<?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\Fluid\ViewHelpers\Be\Menus;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper which returns a select box that can be used to switch between
* multiple actions and controllers and looks similar to TYPO3s "funcMenu".
*
* ```
* <f:be.menus.actionMenu>
* <f:be.menus.actionMenuItem label="First Menu" controller="Default" action="index" />
* <f:be.menus.actionMenuItemGroup label="Information">
* <f:be.menus.actionMenuItem label="PHP Information" controller="Information" action="listPhpInfo" />
* <f:be.menus.actionMenuItem label="{f:translate(key:'documentation')}" controller="Information" action="documentation" />
* ...
* </f:be.menus.actionMenuItemGroup>
* </f:be.menus.actionMenu>
* ```
*
* **Note:** This ViewHelper is experimental and tailored to be used only in extbase context.
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-menus-actionmenu
*/
final class ActionMenuViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'select';
public function __construct(
private readonly PageRenderer $pageRenderer
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('defaultController', 'string', 'The default controller to be used');
}
public function render(): string
{
$options = '';
foreach ($this->viewHelperNode->getChildNodes() as $childNode) {
if ($childNode instanceof ViewHelperNode) {
$options .= $childNode->evaluate($this->renderingContext);
}
}
$this->tag->addAttributes([
'data-global-event' => 'change',
'data-action-navigate' => '$value',
]);
$this->tag->setContent($options);
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/global-event-handler.js');
return '<div class="docheader-funcmenu">' . $this->tag->render() . '</div>';
}
/**
* @param string $argumentsName
* @param string $closureName
* @param string $initializationPhpCode
*/
public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler): string
{
// @todo: replace with a true compiling method to make compilable!
$compiler->disable();
return '';
}
}
@@ -0,0 +1,80 @@
<?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\Fluid\ViewHelpers\Be;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper which returns the page info icon as known from TYPO3 backend modules.
*
* ```
* <f:be.pageInfo />
* ```
*
* **Note:** This ViewHelper is experimental!
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-pageinfo
* @todo: Candidate to deprecate? The page info is typically displayed in doc header, done by ModuleTemplate in controllers.
*/
final class PageInfoViewHelper extends AbstractViewHelper
{
/**
* This ViewHelper renders HTML, thus output must not be escaped
*
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly IconFactory $iconFactory
) {}
public function render(): string
{
$id = 0;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$id = $request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0;
}
$pageRecord = BackendUtility::readPageAccess($id, $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW));
// Add icon with context menu, etc:
if (is_array($pageRecord) && ($pageRecord['uid'] ?? false)) {
// If there IS a real page
$altText = BackendUtility::getRecordIconAltText($pageRecord, 'pages');
$theIcon = '<span title="' . $altText . '">' . $this->iconFactory->getIconForRecord('pages', $pageRecord, IconSize::SMALL)->render() . '</span>';
// Make Icon:
$theIcon = BackendUtility::wrapClickMenuOnIcon($theIcon, 'pages', $pageRecord['uid']);
// Setting icon with context menu + uid
$theIcon .= ' <em>[PID: ' . $pageRecord['uid'] . ']</em>';
} else {
// On root-level of page tree
// Make Icon
$theIcon = '<span title="' . htmlspecialchars($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']) . '">' . $this->iconFactory->getIcon('apps-pagetree-page-domain', IconSize::SMALL)->render() . '</span>';
if ($GLOBALS['BE_USER']->isAdmin()) {
$theIcon = BackendUtility::wrapClickMenuOnIcon($theIcon, 'pages');
}
}
return $theIcon;
}
}
@@ -0,0 +1,88 @@
<?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\Fluid\ViewHelpers\Be;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3Fluid\Fluid\Core\Parser\ParsingState;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperNodeInitializedEventInterface;
/**
* ViewHelper which returns the current page path as known from TYPO3 backend modules.
*
* ```
* <f:be.pagePath />
* ```
*
* **Note:** This ViewHelper is experimental!
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-pagepath
* @deprecated since TYPO3 v15.0, will be removed in TYPO3 v16.0.
*/
final class PagePathViewHelper extends AbstractViewHelper implements ViewHelperNodeInitializedEventInterface
{
/**
* This ViewHelper renders HTML, thus output must not be escaped
*
* @var bool
*/
protected $escapeOutput = false;
public function render(): string
{
$id = 0;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$id = $request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0;
}
$pageRecord = BackendUtility::readPageAccess($id, $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW));
// Is this a real page
if ($pageRecord['_thePathFull'] ?? false) {
$title = (string)$pageRecord['_thePathFull'];
} else {
$title = (string)$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
}
// Setting the path of the page
$pagePath = htmlspecialchars(self::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.path')) . ': <span class="typo3-docheader-pagePath">';
$croppedTitle = BackendUtility::cropToTitleLength($title, null, true);
if ($croppedTitle !== $title) {
$pagePath .= '<abbr title="' . htmlspecialchars($title) . '">' . htmlspecialchars($croppedTitle) . '</abbr>';
} else {
$pagePath .= htmlspecialchars($title);
}
$pagePath .= '</span>';
return $pagePath;
}
public static function nodeInitializedEvent(ViewHelperNode $node, array $arguments, ParsingState $parsingState): void
{
trigger_error(
'<f:be.pagePath> has been deprecated in TYPO3 v15.0 and will be removed in v16.0.',
E_USER_DEPRECATED
);
}
private static function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,114 @@
<?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\Fluid\ViewHelpers\Be;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to register backend module resources like CSS and JavaScript using the PageRenderer.
*
* ```
* <f:be.pageRenderer
* pageTitle="foo"
* includeCssFiles="{0: 'EXT:my_ext/Resources/Public/Css/Stylesheet.css'}"
* includeJsFiles="{0: 'EXT:my_ext/Resources/Public/JavaScript/Library1.js', 1: 'EXT:my_ext/Resources/Public/JavaScript/Library2.js'}"
* addJsInlineLabels="{'my_ext.label1': 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:label1'}"
* includeJavaScriptModules="{0: '@my-vendor/my-ext/my-module.js'}"
* addInlineSettings="{'some.setting.key': 'some.setting.value'}"
* />
* ```
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-pagerenderer
*/
final class PageRendererViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly PageRenderer $pageRenderer
) {}
public function initializeArguments(): void
{
$this->registerArgument('pageTitle', 'string', 'title tag of the module. Not required by default, as BE modules are shown in a frame', false, '');
$this->registerArgument('includeCssFiles', 'array', 'List of custom CSS file to be loaded');
$this->registerArgument('includeJsFiles', 'array', 'List of custom JavaScript file to be loaded');
$this->registerArgument('addJsInlineLabels', 'array', 'Custom labels to add to JavaScript inline labels');
$this->registerArgument('includeJavaScriptModules', 'array', 'List of JavaScript modules to be loaded');
$this->registerArgument('addInlineSettings', 'array', 'Adds Javascript Inline Setting');
}
public function render(): string
{
$pageTitle = $this->arguments['pageTitle'];
$includeCssFiles = $this->arguments['includeCssFiles'];
$includeJsFiles = $this->arguments['includeJsFiles'];
$addJsInlineLabels = $this->arguments['addJsInlineLabels'];
$includeJavaScriptModules = $this->arguments['includeJavaScriptModules'];
$addInlineSettings = $this->arguments['addInlineSettings'];
if ($pageTitle) {
$this->pageRenderer->setTitle($pageTitle);
}
// Include custom CSS and JS files
if (is_array($includeCssFiles)) {
foreach ($includeCssFiles as $addCssFile) {
$this->pageRenderer->addCssFile($addCssFile);
}
}
if (is_array($includeJsFiles)) {
foreach ($includeJsFiles as $addJsFile) {
$this->pageRenderer->addJsFile($addJsFile);
}
}
if (is_array($includeJavaScriptModules)) {
foreach ($includeJavaScriptModules as $addJavaScriptModule) {
$this->pageRenderer->loadJavaScriptModule($addJavaScriptModule);
}
}
if (is_array($addInlineSettings)) {
$this->pageRenderer->addInlineSettingArray('', $addInlineSettings);
}
// Add inline language labels
if (is_array($addJsInlineLabels) && count($addJsInlineLabels) > 0) {
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)
&& $this->renderingContext->getAttribute(ServerRequestInterface::class) instanceof RequestInterface) {
// Extbase request resolves extension key and allows overriding labels using TypoScript configuration.
$extensionKey = $this->renderingContext->getAttribute(ServerRequestInterface::class)->getControllerExtensionKey();
foreach ($addJsInlineLabels as $key) {
$label = LocalizationUtility::translate($key, $extensionKey);
$this->pageRenderer->addInlineLanguageLabel($key, $label);
}
} else {
// No extbase request, labels should follow "LLL:EXT:some_ext/Resources/Private/someFile.xlf:key"
// syntax, and are not overridden by TypoScript extbase module / plugin configuration.
foreach ($addJsInlineLabels as &$labelKey) {
$labelKey = self::getLanguageService()->sL($labelKey);
}
$this->pageRenderer->addInlineLanguageLabelArray($addJsInlineLabels);
}
}
return '';
}
private static function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,46 @@
<?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\Fluid\ViewHelpers\Be\Security;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper;
/**
* ViewHelper implementing an ifAuthenticated/else condition for backend
* users and backend groups.
*
* ```
* <f:be.security.ifAuthenticated>
* <f:then>
* This is being shown in case you have access.
* </f:then>
* <f:else>
* This is being displayed in case you do not have access.
* </f:else>
* </f:be.security.ifAuthenticated>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-security-ifauthenticated
*/
final class IfAuthenticatedViewHelper extends AbstractConditionViewHelper
{
public static function verdict(array $arguments, RenderingContextInterface $renderingContext): bool
{
return isset($GLOBALS['BE_USER']) && $GLOBALS['BE_USER']->user['uid'] > 0;
}
}
@@ -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!
*/
namespace TYPO3\CMS\Fluid\ViewHelpers\Be\Security;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper;
/**
* ViewHelper implementing an ifHasRole/else condition for backend users
* and backend groups ("Role").
*
* ```
* <f:be.security.ifHasRole role="Administrator">
* <f:then>
* This is being shown in case you have the role.
* </f:then>
* <f:else>
* This is being displayed in case you do not have the role.
* </f:else>
* </f:be.security.ifHasRole>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-security-ifhasrole
*/
final class IfHasRoleViewHelper extends AbstractConditionViewHelper
{
/**
* Initializes the "role" argument.
* Renders <f:then> child if the current logged in BE user belongs to the specified role (aka usergroup)
* otherwise renders <f:else> child.
*/
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('role', 'string', 'The usergroup (either the usergroup uid or its title).');
}
public static function verdict(array $arguments, RenderingContextInterface $renderingContext): bool
{
$role = $arguments['role'];
if (!is_array($GLOBALS['BE_USER']->userGroups) || $arguments['role'] === null) {
return false;
}
if (is_numeric($role)) {
foreach ($GLOBALS['BE_USER']->userGroups as $userGroup) {
if ((int)$userGroup['uid'] === (int)$role) {
return true;
}
}
} else {
foreach ($GLOBALS['BE_USER']->userGroups as $userGroup) {
if ($userGroup['title'] === $role) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,160 @@
<?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\Fluid\ViewHelpers\Be;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Module\ModuleData;
use TYPO3\CMS\Backend\RecordList\DatabaseRecordList;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper which renders a record list as known from the TYPO3 records module.
*
* ```
* <f:be.tableList tableName="fe_users"
* fieldList="{0: 'name', 1: 'email'}"
* storagePid="1"
* levels="2"
* filter="foo"
* recordsPerPage="10"
* sortField="name"
* sortDescending="true"
* readOnly="true"
* enableClickMenu="false"
* enableControlPanels="true"
* clickTitleMode="info"
* />
* ```
*
* **Note:** This feature is experimental!
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-tablelist
*/
final class TableListViewHelper extends AbstractViewHelper
{
/**
* As this ViewHelper renders HTML, the output must not be escaped.
*
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly ConfigurationManagerInterface $configurationManager,
private readonly PageRenderer $pageRenderer,
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('tableName', 'string', 'name of the database table', true);
$this->registerArgument('fieldList', 'array', 'list of fields to be displayed. If empty, only the title column (configured in $TCA[$tableName][\'ctrl\'][\'title\']) is shown', false, []);
$this->registerArgument('storagePid', 'int', 'by default, records are fetched from the storage PID configured in persistence.storagePid. With this argument, the storage PID can be overwritten');
$this->registerArgument('levels', 'int', 'corresponds to the level selector of the TYPO3 records module. By default only records from the current storagePid are fetched', false, 0);
$this->registerArgument('filter', 'string', 'corresponds to the "Search String" textbox of the TYPO3 records module. If not empty, only records matching the string will be fetched', false, '');
$this->registerArgument('recordsPerPage', 'int', 'amount of records to be displayed at once. Defaults to 100', false, 0);
$this->registerArgument('sortField', 'string', 'table field to sort the results by', false, '');
$this->registerArgument('sortDescending', 'bool', 'if TRUE records will be sorted in descending order', false, false);
$this->registerArgument('readOnly', 'bool', 'if TRUE, the edit icons won\'t be shown. Otherwise edit icons will be shown, if the current BE user has edit rights for the specified table!', false, false);
$this->registerArgument('enableClickMenu', 'bool', 'enables context menu', false, true);
$this->registerArgument('enableControlPanels', 'bool', 'enables control panels', false, false);
$this->registerArgument('clickTitleMode', 'string', 'one of "edit", "show" (only pages, tt_content), "info', false, '');
}
/**
* Renders a record list as known from the TYPO3 records module
* Note: This feature is experimental!
*
* @see DatabaseRecordList
*/
public function render(): string
{
$tableName = $this->arguments['tableName'];
$fieldList = $this->arguments['fieldList'];
$storagePid = $this->arguments['storagePid'];
$levels = $this->arguments['levels'];
$filter = $this->arguments['filter'];
$recordsPerPage = $this->arguments['recordsPerPage'];
$sortField = $this->arguments['sortField'];
$sortDescending = $this->arguments['sortDescending'];
$readOnly = $this->arguments['readOnly'];
$enableClickMenu = $this->arguments['enableClickMenu'];
$clickTitleMode = $this->arguments['clickTitleMode'];
$enableControlPanels = $this->arguments['enableControlPanels'];
$backendUser = $this->getBackendUser();
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
// All views in backend have at least ServerRequestInterface. Should be fine
// to assume having a request here, the early return is just sanitation.
return '';
}
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/recordlist.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/record-download-button.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/action-dispatcher.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/page-wizard/new-page-wizard-button.js');
if ($enableControlPanels === true) {
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/multi-record-selection.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/multi-record-selection-delete-action.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/context-menu.js');
}
$pageId = (int)($request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0);
$pointer = (int)($request->getParsedBody()['pointer'] ?? $request->getQueryParams()['pointer'] ?? 0);
$pageInfo = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: [];
$existingModuleData = $backendUser->getModuleData('records');
$moduleData = new ModuleData('records', is_array($existingModuleData) ? $existingModuleData : []);
$dbList = GeneralUtility::makeInstance(DatabaseRecordList::class);
$dbList->setRequest($request->withoutAttribute('pageContext'));
$dbList->setModuleData($moduleData);
$dbList->pageRow = $pageInfo;
if ($readOnly) {
$dbList->setIsEditable(false);
} else {
$dbList->calcPerms = new Permission($backendUser->calcPerms($pageInfo));
}
$dbList->disableSingleTableView = true;
$dbList->clickTitleMode = $clickTitleMode;
$dbList->clickMenuEnabled = $enableClickMenu;
if ($storagePid === null) {
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
$storagePid = $frameworkConfiguration['persistence']['storagePid'];
}
$dbList->start($storagePid, $tableName, $pointer, $filter, $levels, $recordsPerPage);
// Column selector is disabled since fields are defined by the "fieldList" argument
$dbList->displayColumnSelector = false;
$dbList->setFields = [$tableName => $fieldList];
$dbList->noControlPanels = !$enableControlPanels;
$dbList->sortField = $sortField;
$dbList->sortRev = $sortDescending;
return $dbList->generateList();
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
+60
View File
@@ -0,0 +1,60 @@
<?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\Fluid\ViewHelpers\Be;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper for creating URIs to backend modules.
*
* ```
* <f:be.uri route="web_ts" parameters="{id: 92}" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-be-uri
*/
final class UriViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly UriBuilder $uriBuilder
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('route', 'string', 'The name of the route', true);
$this->registerArgument('parameters', 'array', 'An array of parameters', false, []);
$this->registerArgument(
'referenceType',
'string',
'The type of reference to be generated (one of the constants)',
false,
UriBuilder::ABSOLUTE_PATH
);
}
public function render(): string
{
$route = $this->arguments['route'];
$parameters = $this->arguments['parameters'];
$referenceType = $this->arguments['referenceType'];
$uri = $this->uriBuilder->buildUriFromRoute($route, $parameters, $referenceType);
return (string)$uri;
}
}
+148
View File
@@ -0,0 +1,148 @@
<?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\Fluid\ViewHelpers;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to render CObjects (objects containing rendering definitions for records/elements),
* using the global TypoScript configuration.
*
* ```
* <f:cObject typoscriptObjectPath="lib.someLibObject" />
* ```
*
* **Note:** You have to ensure proper escaping (`htmlspecialchars`/`intval`/etc.) on your own!
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-cobject
*/
final class CObjectViewHelper extends AbstractViewHelper
{
/**
* Disable escaping of child nodes' output
*
* @var bool
*/
protected $escapeChildren = false;
/**
* Disable escaping of this node's output
*
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly TimeTracker $timeTracker,
private readonly ConfigurationManagerInterface $configurationManager,
) {}
public function initializeArguments(): void
{
$this->registerArgument('data', 'mixed', 'the data to be used for rendering the cObject. Can be an object, array or string. If this argument is not set, child nodes will be used');
$this->registerArgument('typoscriptObjectPath', 'string', 'the TypoScript setup path of the TypoScript object to render', true);
$this->registerArgument('currentValueKey', 'string', 'currentValueKey');
$this->registerArgument('table', 'string', 'the table name associated with "data" argument. Typically tt_content or one of your custom tables. This argument should be set if rendering a FILES cObject where file references are used, or if the data argument is a database record.', false, '');
}
/**
* Renders the TypoScript object in the given TypoScript setup path.
*/
public function render(): string
{
$data = $this->renderChildren() ?? [];
$typoscriptObjectPath = (string)$this->arguments['typoscriptObjectPath'];
$currentValueKey = $this->arguments['currentValueKey'];
$table = $this->arguments['table'];
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
throw new \RuntimeException('Required request not found in RenderingContext', 1724243608);
}
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObjectRenderer->setRequest($request);
$parent = $request->getAttribute('currentContentObject');
if ($parent instanceof ContentObjectRenderer) {
$contentObjectRenderer->setParent($parent->data, $parent->currentRecord);
}
$currentValue = null;
if (is_object($data)) {
$data = $data instanceof RecordInterface ? ($data->getRawRecord()?->toArray(true) ?? $data->toArray()) : ObjectAccess::getGettableProperties($data);
} elseif (is_string($data) || is_numeric($data)) {
$currentValue = (string)$data;
$data = [$data];
}
$contentObjectRenderer->start($data, $table);
if ($currentValue !== null) {
$contentObjectRenderer->setCurrentVal($currentValue);
} elseif ($currentValueKey !== null && isset($data[$currentValueKey])) {
$contentObjectRenderer->setCurrentVal($data[$currentValueKey]);
}
$pathSegments = GeneralUtility::trimExplode('.', $typoscriptObjectPath);
$lastSegment = (string)array_pop($pathSegments);
$setup = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT);
foreach ($pathSegments as $segment) {
if (!array_key_exists($segment . '.', $setup)) {
throw new InvalidArgumentValueException(
'TypoScript object path "' . $typoscriptObjectPath . '" does not exist',
1253191023
);
}
$setup = $setup[$segment . '.'];
}
if (!isset($setup[$lastSegment])) {
throw new InvalidArgumentValueException(
'No Content Object definition found at TypoScript object path "' . $typoscriptObjectPath . '"',
1540246570
);
}
return $this->renderContentObject($contentObjectRenderer, $setup, $typoscriptObjectPath, $lastSegment);
}
/**
* Renders single content object and increases time tracker stack pointer
*/
private function renderContentObject(ContentObjectRenderer $contentObjectRenderer, array $setup, string $typoscriptObjectPath, string $lastSegment): string
{
if ($this->timeTracker->LR) {
$this->timeTracker->push('/f:cObject/', '<' . $typoscriptObjectPath);
}
$this->timeTracker->incStackPointer();
$content = $contentObjectRenderer->cObjGetSingle($setup[$lastSegment], $setup[$lastSegment . '.'] ?? [], $typoscriptObjectPath);
$this->timeTracker->decStackPointer();
if ($this->timeTracker->LR) {
$this->timeTracker->pull($content);
}
return $content;
}
/**
* Explicitly set argument name to be used as content.
*/
public function getContentArgumentName(): string
{
return 'data';
}
}
+86
View File
@@ -0,0 +1,86 @@
<?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\Fluid\ViewHelpers;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to generate an HTML readable dump of variables or objects.
* The output can be navigated like a nested tree. The output will be put
* at the beginning of the HTML response, unless the `inline` attribute is set,
* so that the output will be placed at the specific place where it is placed
* inside a Fluid template.
*
* ```
* <f:debug title="My Title"
* maxDepth="5"
* blacklistedClassNames="{0:'ACME\BlogExample\Domain\Model\Administrator'}"
* blacklistedPropertyNames="{0:'posts'}"
* plainText="true"
* ansiColors="false"
* inline="true">{blogs}
* </f:debug>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-debug
*/
final class DebugViewHelper extends AbstractViewHelper
{
/**
* This prevents double escaping as the output is encoded in DebuggerUtility::var_dump
*
* @var bool
*/
protected $escapeChildren = false;
/**
* Output of this viewhelper is already escaped
*
* @var bool
*/
protected $escapeOutput = false;
public function initializeArguments(): void
{
$this->registerArgument('title', 'string', 'optional custom title for the debug output');
$this->registerArgument('maxDepth', 'int', 'Sets the max recursion depth of the dump (defaults to 8). De- or increase the number according to your needs and memory limit.', false, 8);
$this->registerArgument('plainText', 'bool', 'If TRUE, the dump is in plain text, if FALSE the debug output is in HTML format.', false, false);
$this->registerArgument('ansiColors', 'bool', 'If TRUE, ANSI color codes is added to the plaintext output, if FALSE (default) the plaintext debug output not colored.', false, false);
$this->registerArgument('inline', 'bool', 'if TRUE, the dump is rendered at the position of the <f:debug> tag. If FALSE (default), the dump is displayed at the top of the page.', false, false);
$this->registerArgument('blacklistedClassNames', 'array', 'An array of class names (RegEx) to be filtered. Default is an array of some common class names.');
$this->registerArgument('blacklistedPropertyNames', 'array', 'An array of property names and/or array keys (RegEx) to be filtered. Default is an array of some common property names.');
}
/**
* A wrapper for \TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump().
*/
public function render(): string
{
return DebuggerUtility::var_dump(
$this->renderChildren(),
is_scalar($this->arguments['title']) ? (string)$this->arguments['title'] : null,
(int)$this->arguments['maxDepth'],
(bool)$this->arguments['plainText'],
(bool)$this->arguments['ansiColors'],
(bool)$this->arguments['inline'],
is_array($this->arguments['blacklistedClassNames']) ? $this->arguments['blacklistedClassNames'] : null,
is_array($this->arguments['blacklistedPropertyNames']) ? $this->arguments['blacklistedPropertyNames'] : null
);
}
}
+54
View File
@@ -0,0 +1,54 @@
<?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\Fluid\ViewHelpers;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper;
/**
* ViewHelper to check if a feature flag is enabled, implemented as
* a condition like an "if" construct.
*
* ```
* <f:feature name="myFeatureFlag">
* <f:then>
* Flag is enabled
* </f:then>
* <f:else>
* Flag is undefined or not enabled
* </f:else>
* </f:feature>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-feature
*/
final class FeatureViewHelper extends AbstractConditionViewHelper
{
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('name', 'string', 'name of the feature flag that should be checked', true);
}
public static function verdict(array $arguments, RenderingContextInterface $renderingContext): bool
{
return GeneralUtility::makeInstance(Features::class)->isFeatureEnabled($arguments['name']);
}
}
@@ -0,0 +1,113 @@
<?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\Fluid\ViewHelpers;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Messaging\FlashMessageRendererResolver;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Service\ExtensionService;
use TYPO3Fluid\Fluid\Core\Variables\ScopedVariableProvider;
use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper which renders the flash messages (output messages / message bubbles, which can also
* be queued from a preceding request). No output occurs if no flash messages are
* queued. Output is done with a hard-coded HTML definition, but the raw contents can be
* extracted via the `as` attribute, and rendered with custom formatting.
*
* ```
* <f:flashMessages />
*
* <f:flashMessages as="flashMessages">
* <dl class="messages">
* <f:for each="{flashMessages}" as="flashMessage">
* <dt>{flashMessage.code}</dt>
* <dd>{flashMessage.message}</dd>
* </f:for>
* </dl>
* </f:flashMessages>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-flashmessages
*/
final class FlashMessagesViewHelper extends AbstractViewHelper
{
/**
* ViewHelper outputs HTML therefore output escaping has to be disabled
*
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly FlashMessageService $flashMessageService,
private readonly FlashMessageRendererResolver $flashMessageRendererResolver,
private readonly ExtensionService $extensionService
) {}
public function initializeArguments(): void
{
$this->registerArgument('queueIdentifier', 'string', 'Flash-message queue to use');
$this->registerArgument('as', 'string', 'The name of the current flashMessage variable for rendering inside');
}
/**
* Renders FlashMessages and flushes the FlashMessage queue
*
* Note: This does not disable the current page cache in order to prevent FlashMessage output
* from being cached.
* In case of conditional flash message rendering, caching must be disabled
* (e.g. for a controller action).
* Custom caching using the Caching Framework can be used in this case.
*/
public function render(): string
{
$as = $this->arguments['as'];
$queueIdentifier = $this->arguments['queueIdentifier'];
if ($queueIdentifier === null) {
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)
|| !$this->renderingContext->getAttribute(ServerRequestInterface::class) instanceof RequestInterface
) {
// Throw if not an extbase request
throw new \RuntimeException(
'ViewHelper f:flashMessages needs an extbase Request object to resolve the Queue identifier magically.'
. ' When not in extbase context, set attribute "queueIdentifier".',
1639821269
);
}
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$pluginNamespace = $this->extensionService->getPluginNamespace($request->getControllerExtensionName(), $request->getPluginName());
$queueIdentifier = 'extbase.flashmessages.' . $pluginNamespace;
}
$flashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier($queueIdentifier);
$flashMessages = $flashMessageQueue->getAllMessagesAndFlush();
if (count($flashMessages) === 0) {
return '';
}
if ($as === null) {
return $this->flashMessageRendererResolver->resolve()->render($flashMessages);
}
$variableProvider = new ScopedVariableProvider($this->renderingContext->getVariableProvider(), new StandardVariableProvider([$as => $flashMessages]));
$this->renderingContext->setVariableProvider($variableProvider);
$content = (string)$this->renderChildren();
$this->renderingContext->setVariableProvider($variableProvider->getGlobalVariableProvider());
return $content;
}
}
@@ -0,0 +1,404 @@
<?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\Fluid\ViewHelpers\Form;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
use TYPO3\CMS\Extbase\Error\Result;
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper;
/**
* Abstract Form ViewHelper. Bundles functionality related to direct property access of objects in other Form ViewHelpers.
*
* If you set the "property" attribute to the name of the property to resolve from the object, this class will
* automatically set the name and value of a form element.
*
* Note this set of ViewHelpers is tailored to be used only in extbase context.
*/
abstract class AbstractFormFieldViewHelper extends AbstractFormViewHelper
{
protected ConfigurationManagerInterface $configurationManager;
protected bool $respectSubmittedDataValue = false;
public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
{
$this->configurationManager = $configurationManager;
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('name', 'string', 'Name of input tag');
$this->registerArgument('value', 'mixed', 'Value of input tag');
$this->registerArgument('property', 'string', 'Name of Object Property. If used in conjunction with <f:form object="...">, the "name" property will be ignored, while "value" can be used to specify a default field value instead of the object property value.');
}
/**
* Getting the current configuration for respectSubmittedDataValue.
*/
public function getRespectSubmittedDataValue(): bool
{
return $this->respectSubmittedDataValue;
}
/**
* Define respectSubmittedDataValue to enable or disable the usage of the submitted values in the viewhelper.
*/
public function setRespectSubmittedDataValue(bool $respectSubmittedDataValue): void
{
$this->respectSubmittedDataValue = $respectSubmittedDataValue;
}
/**
* Get the name of this form element.
* Either returns arguments['name'], or the correct name for Object Access.
* In case property is something like bla.blubb (hierarchical), then [bla][blubb] is generated.
*/
protected function getName(): string
{
$name = $this->getNameWithoutPrefix();
return $this->prefixFieldName($name);
}
/**
* Shortcut for retrieving the request from the controller context
*
* @return RequestInterface The extbase (!) request. All these VH's are extbase-only.
*/
protected function getRequest(): RequestInterface
{
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)
|| !$this->renderingContext->getAttribute(ServerRequestInterface::class) instanceof RequestInterface
) {
throw new \RuntimeException(
'Form ViewHelpers are Extbase specific and need an Extbase Request to work',
1663617170
);
}
return $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
/**
* Get the name of this form element, without prefix.
*/
protected function getNameWithoutPrefix(): string
{
if ($this->isObjectAccessorMode()) {
$formObjectName = $this->renderingContext->getViewHelperVariableContainer()->get(
FormViewHelper::class,
'formObjectName'
);
if (!empty($formObjectName)) {
$propertySegments = explode('.', (string)($this->arguments['property'] ?? ''));
$propertyPath = '';
foreach ($propertySegments as $segment) {
$propertyPath .= '[' . $segment . ']';
}
$name = $formObjectName . $propertyPath;
} else {
$name = $this->arguments['property'] ?? '';
}
} else {
$name = $this->arguments['name'] ?? '';
}
if ($this->hasArgument('value')
&& is_object($this->arguments['value'])
&& !$this->persistenceManager->isNewObject($this->arguments['value'])
) {
$name .= '[__identity]';
}
return (string)$name;
}
/**
* Returns the current value of this Form ViewHelper and converts it to an identifier string in case it's an object
* The value is determined as follows:
* * If property mapping errors occurred and the form is re-displayed, the *last submitted* value is returned
* * If a "value" attribute was specified, this value is used (preferring an "override" from integrators)
* * Else the bound property value is returned (only in objectAccessor-mode)
*
* Note: This method should *not* be used for form elements that must not change the value attribute, e.g. (radio) buttons and checkboxes.
*
* @return mixed Value
*/
protected function getValueAttribute()
{
$value = null;
if ($this->respectSubmittedDataValue) {
$value = $this->getValueFromSubmittedFormData($value);
} elseif ($this->hasArgument('value')) {
$value = $this->arguments['value'];
} elseif ($this->isObjectAccessorMode()) {
$value = $this->getPropertyValue();
}
$value = $this->convertToPlainValue($value);
return $value;
}
/**
* If property mapping errors occurred and the form is re-displayed, the *last submitted* value is returned by this
* method.
*
* Note:
* This method should *not* be used for form elements that must not change the value attribute, e.g. (radio)
* buttons and checkboxes. The default behaviour is not to use this method. You need to set
* respectSubmittedDataValue to TRUE to enable the form data handling for the viewhelper.
*
* @param mixed $value
* @return mixed Value
*/
protected function getValueFromSubmittedFormData($value)
{
$submittedFormData = null;
if ($this->hasMappingErrorOccurred()) {
$submittedFormData = $this->getLastSubmittedFormData();
}
if ($submittedFormData !== null) {
$value = $submittedFormData;
} elseif ($this->hasArgument('value')) {
$value = $this->arguments['value'];
} elseif ($this->isObjectAccessorMode()) {
$value = $this->getPropertyValue();
}
return $value;
}
/**
* Converts an arbitrary value to a plain value
*
* @param mixed $value The value to convert
* @return mixed
*/
protected function convertToPlainValue($value)
{
if (is_object($value)) {
if ($value instanceof DomainObjectInterface && $value->getUid() !== null) {
// We prefer to use the `getUid()` method because this returns the properly overlaid identifier (defaultLanguageRecordUid).
// Otherwise, an identifier would contain '[defaultLanguageRecordUid]_[localizedRecordUid]'. This in turn
// will not properly trigger the select option "is selected" comparison.
// @see SelectViewHelper->getOptionValueScalar()
return $value->getUid();
}
$identifier = $this->persistenceManager->getIdentifierByObject($value);
if ($identifier !== null) {
return $identifier;
}
}
return $value;
}
/**
* Checks if a property mapping error has occurred in the last request.
*/
protected function hasMappingErrorOccurred(): bool
{
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
$extbaseRequestParameters = $this->getRequest()->getAttribute('extbase');
return $extbaseRequestParameters->getOriginalRequest() !== null;
}
/**
* Get the form data which has last been submitted; only returns valid data in case
* a property mapping error has occurred. Check with hasMappingErrorOccurred() before!
*
* @return mixed
*/
protected function getLastSubmittedFormData()
{
$propertyPath = rtrim(preg_replace('/(\\]\\[|\\[|\\])/', '.', $this->getNameWithoutPrefix()) ?? '', '.');
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
$extbaseRequestParameters = $this->getRequest()->getAttribute('extbase');
$value = ObjectAccess::getPropertyPath(
$extbaseRequestParameters->getOriginalRequest()->getArguments(),
$propertyPath
);
return $value;
}
/**
* Add additional identity properties in case the current property is hierarchical (of the form "bla.blubb").
* Then, [bla][__identity] has to be generated as well.
*/
protected function addAdditionalIdentityPropertiesIfNeeded(): void
{
if (!$this->isObjectAccessorMode()) {
return;
}
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if (!$viewHelperVariableContainer->exists(
FormViewHelper::class,
'formObject'
)
) {
return;
}
$propertySegments = explode('.', (string)($this->arguments['property'] ?? ''));
// hierarchical property. If there is no "." inside (thus $propertySegments == 1), we do not need to do anything
if (count($propertySegments) < 2) {
return;
}
$formObject = $viewHelperVariableContainer->get(
FormViewHelper::class,
'formObject'
);
$objectName = $viewHelperVariableContainer->get(
FormViewHelper::class,
'formObjectName'
);
// If count == 2 -> we need to go through the for-loop exactly once
$propertySegmentsCount = count($propertySegments);
for ($i = 1; $i < $propertySegmentsCount; $i++) {
$object = ObjectAccess::getPropertyPath($formObject, implode('.', array_slice($propertySegments, 0, $i)));
if (!is_object($object)) {
$object = null;
}
$objectName .= '[' . $propertySegments[$i - 1] . ']';
$hiddenIdentityField = $this->renderHiddenIdentityField($object, $objectName);
// Add the hidden identity field to the ViewHelperVariableContainer
$additionalIdentityProperties = $viewHelperVariableContainer->get(
FormViewHelper::class,
'additionalIdentityProperties'
);
$additionalIdentityProperties[$objectName] = $hiddenIdentityField;
$viewHelperVariableContainer->addOrUpdate(
FormViewHelper::class,
'additionalIdentityProperties',
$additionalIdentityProperties
);
}
}
/**
* Get the current property of the object bound to this form.
*
* @return mixed Value
*/
protected function getPropertyValue()
{
if (!isset($this->arguments['property'])) {
return null;
}
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if (!$viewHelperVariableContainer->exists(
FormViewHelper::class,
'formObject'
)
) {
return null;
}
$formObject = $viewHelperVariableContainer->get(
FormViewHelper::class,
'formObject'
);
return ObjectAccess::getPropertyPath($formObject, (string)$this->arguments['property']);
}
/**
* Internal method which checks if we should evaluate a domain object or just output arguments['name']
* and arguments['value']. Returns true if domain object should be evaluated.
*/
protected function isObjectAccessorMode(): bool
{
return $this->hasArgument('property') && $this->renderingContext->getViewHelperVariableContainer()->exists(
FormViewHelper::class,
'formObjectName'
);
}
/**
* Add a CSS class if this ViewHelper has errors
*/
protected function setErrorClassAttribute(): void
{
if (isset($this->additionalArguments['class'])) {
$cssClass = $this->additionalArguments['class'] . ' ';
} else {
$cssClass = '';
}
$mappingResultsForProperty = $this->getMappingResultsForProperty();
if ($mappingResultsForProperty->hasErrors()) {
if ($this->hasArgument('errorClass')) {
$cssClass .= $this->arguments['errorClass'];
} else {
$cssClass .= 'error';
}
$this->tag->addAttribute('class', $cssClass);
}
}
/**
* Get errors for the property and form name of this ViewHelper
*/
protected function getMappingResultsForProperty(): Result
{
if (!$this->isObjectAccessorMode()) {
return new Result();
}
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
$extbaseRequestParameters = $this->getRequest()->getAttribute('extbase');
$originalRequestMappingResults = $extbaseRequestParameters->getOriginalRequestMappingResults();
$formObjectName = $this->renderingContext->getViewHelperVariableContainer()->get(
FormViewHelper::class,
'formObjectName'
);
return $originalRequestMappingResults->forProperty($formObjectName)->forProperty((string)$this->arguments['property']);
}
/**
* Renders a hidden field with the same name as the element, to make sure the empty value is submitted
* in case nothing is selected. This is needed for checkbox and multiple select fields
*/
protected function renderHiddenFieldForEmptyValue(): string
{
$hiddenFieldNames = [];
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if ($viewHelperVariableContainer->exists(
FormViewHelper::class,
'renderedHiddenFields'
)
) {
$hiddenFieldNames = $viewHelperVariableContainer->get(
FormViewHelper::class,
'renderedHiddenFields'
);
}
$fieldName = $this->getName();
if (substr($fieldName, -2) === '[]') {
$fieldName = substr($fieldName, 0, -2);
}
if (!in_array($fieldName, $hiddenFieldNames, true)) {
$hiddenFieldNames[] = $fieldName;
$viewHelperVariableContainer->addOrUpdate(
FormViewHelper::class,
'renderedHiddenFields',
$hiddenFieldNames
);
return '<input type="hidden" name="' . htmlspecialchars($fieldName) . '" value="" />';
}
return '';
}
}
@@ -0,0 +1,119 @@
<?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\Fluid\ViewHelpers\Form;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Type\DocType;
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
use TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* Abstract Form ViewHelper. Bundles functionality related to direct property access of objects in other Form ViewHelpers.
*
* If you set the "property" attribute to the name of the property to resolve from the object, this class will
* automatically set the name and value of a form element.
*
* Note this set of ViewHelpers is tailored to be used only in extbase context.
*/
abstract class AbstractFormViewHelper extends AbstractTagBasedViewHelper
{
protected PersistenceManagerInterface $persistenceManager;
public function injectPersistenceManager(PersistenceManagerInterface $persistenceManager): void
{
$this->persistenceManager = $persistenceManager;
}
/**
* Prefixes / namespaces the given name with the form field prefix
*/
protected function prefixFieldName(string $fieldName): string
{
if ($fieldName === '') {
return '';
}
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if (!$viewHelperVariableContainer->exists(FormViewHelper::class, 'fieldNamePrefix')) {
return $fieldName;
}
$fieldNamePrefix = (string)$viewHelperVariableContainer->get(FormViewHelper::class, 'fieldNamePrefix');
if ($fieldNamePrefix === '') {
return $fieldName;
}
$fieldNameSegments = explode('[', $fieldName, 2);
$fieldName = $fieldNamePrefix . '[' . $fieldNameSegments[0] . ']';
if (count($fieldNameSegments) > 1) {
$fieldName .= '[' . $fieldNameSegments[1];
}
return $fieldName;
}
/**
* Renders a hidden form field containing the technical identity of the given object.
*
* @param mixed $object Object to create the identity field for. Non-objects are ignored.
* @param string|null $name Name
* @return string A hidden field containing the Identity (uid) of the given object
* @see \TYPO3\CMS\Extbase\Mvc\Controller\Argument::setValue()
*/
protected function renderHiddenIdentityField(mixed $object, ?string $name): string
{
if ($object instanceof LazyLoadingProxy) {
$object = $object->_loadRealInstance();
}
if (!is_object($object)
|| !($object instanceof AbstractDomainObject)
|| ($object->_isNew() && !$object->_isClone())) {
return '';
}
// Intentionally NOT using PersistenceManager::getIdentifierByObject here.
// Using that one breaks re-submission of data in forms in case of an error.
$identifier = $object->getUid();
if ($identifier === null) {
return LF . '<!-- Object of type ' . get_class($object) . ' is without identity -->' . LF;
}
$name = $this->prefixFieldName($name ?? '') . '[__identity]';
$this->registerFieldNameForFormTokenGeneration($name);
$endingSlash = ($this->shouldUseXHtmlSlash() ? '/' : '');
return LF . '<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars((string)$identifier) . '" ' . $endingSlash . '>' . LF;
}
/**
* Register a field name for inclusion in the HMAC / Form Token generation
*/
protected function registerFieldNameForFormTokenGeneration(string $fieldName): void
{
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if ($viewHelperVariableContainer->exists(FormViewHelper::class, 'formFieldNames')) {
$formFieldNames = $viewHelperVariableContainer->get(FormViewHelper::class, 'formFieldNames');
} else {
$formFieldNames = [];
}
$formFieldNames[] = $fieldName;
$viewHelperVariableContainer->addOrUpdate(FormViewHelper::class, 'formFieldNames', $formFieldNames);
}
protected function shouldUseXHtmlSlash(): bool
{
return DocType::createFromRequest($this->renderingContext->getAttribute(ServerRequestInterface::class))->isXmlCompliant();
}
}
@@ -0,0 +1,59 @@
<?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\Fluid\ViewHelpers\Form;
/**
* ViewHelper which renders a form button.
*
* ```
* <f:form.button type="reset" disabled="disabled"
* name="buttonName" value="buttonValue"
* formmethod="post" formnovalidate="formnovalidate"
* >Cancel</f:form.button>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-button
*/
final class ButtonViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'button';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('type', 'string', 'Specifies the type of button (e.g. "button", "reset" or "submit")', false, 'submit');
}
public function render(): string
{
$type = $this->arguments['type'];
$name = $this->getName();
$this->registerFieldNameForFormTokenGeneration($name);
$this->tag->addAttribute('type', $type);
$this->tag->addAttribute('name', $name);
$this->tag->addAttribute('value', (string)$this->getValueAttribute());
$this->tag->setContent((string)$this->renderChildren());
$this->tag->forceClosingTag(true);
return $this->tag->render();
}
}
@@ -0,0 +1,95 @@
<?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\Fluid\ViewHelpers\Form;
/**
* ViewHelper which renders a simple checkbox `<input type="checkbox">`.
*
* ```
* <f:form.checkbox property="interests" value="TYPO3" multiple="1" />
* <f:form.checkbox name="interest" value="TYPO3" checked="{object.interest} == 'TYPO3'" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-checkbox
*/
final class CheckboxViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument(
'errorClass',
'string',
'CSS class to set if there are errors for this ViewHelper',
false,
'f3-form-error'
);
$this->registerArgument('value', 'string', 'Value of input tag. Required for checkboxes', true);
$this->registerArgument('checked', 'bool', 'Specifies that the input element should be preselected');
$this->registerArgument('multiple', 'bool', 'Specifies whether this checkbox belongs to a multivalue (is part of a checkbox group)', false, false);
}
public function render(): string
{
$checked = $this->arguments['checked'];
$multiple = $this->arguments['multiple'];
$this->tag->addAttribute('type', 'checkbox');
$nameAttribute = $this->getName();
$valueAttribute = $this->getValueAttribute();
$propertyValue = null;
if ($this->hasMappingErrorOccurred()) {
$propertyValue = $this->getLastSubmittedFormData();
}
if ($checked === null && $propertyValue === null) {
$propertyValue = $this->getPropertyValue();
}
if ($propertyValue instanceof \Traversable) {
$propertyValue = iterator_to_array($propertyValue);
}
if (is_array($propertyValue)) {
$propertyValue = array_map($this->convertToPlainValue(...), $propertyValue);
if ($checked === null) {
$checked = in_array($valueAttribute, $propertyValue);
}
$nameAttribute .= '[]';
} elseif ($multiple === true) {
$nameAttribute .= '[]';
} elseif ($propertyValue !== null) {
$checked = (bool)$propertyValue === (bool)$valueAttribute;
}
$this->registerFieldNameForFormTokenGeneration($nameAttribute);
$this->tag->addAttribute('name', $nameAttribute);
$this->tag->addAttribute('value', (string)$valueAttribute);
if ($checked === true) {
$this->tag->addAttribute('checked', 'checked');
}
$this->setErrorClassAttribute();
$hiddenField = $this->renderHiddenFieldForEmptyValue();
return $hiddenField . $this->tag->render();
}
}
@@ -0,0 +1,221 @@
<?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\Fluid\ViewHelpers\Form;
use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Core\Country\CountryFilter;
use TYPO3\CMS\Core\Country\CountryProvider;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper which renders a `<select>` tag with all or specific countries as options.
*
* ```
* <f:form.countrySelect name="country" value="AT" />
* <f:form.countrySelect name="country" value="DE"
* optionLabelField="localizedOfficialName"
* prioritizedCountries="{0: 'DE', 1: 'AT', 2: 'CH'}"
* alternativeLanguage="fr"
* sortByOptionLabel="true"
* />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-countryselect
*/
final class CountrySelectViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'select';
public function __construct(
private readonly CountryProvider $countryProvider
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('excludeCountries', 'array', 'Array with country codes that should not be shown.', false, []);
$this->registerArgument('onlyCountries', 'array', 'If set, only the country codes in the list are rendered.', false, []);
$this->registerArgument('optionLabelField', 'string', 'If specified, will call the appropriate getter on each object to determine the label. Use "name", "localizedName", "officialName" or "localizedOfficialName"', false, 'localizedName');
$this->registerArgument('sortByOptionLabel', 'boolean', 'If true, List will be sorted by label.', false, false);
$this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
$this->registerArgument('prependOptionLabel', 'string', 'If specified, will provide an option at first position with the specified label.');
$this->registerArgument('prependOptionValue', 'string', 'If specified, will provide an option at first position with the specified value.');
$this->registerArgument('multiple', 'boolean', 'If set multiple options may be selected.', false, false);
$this->registerArgument('required', 'boolean', 'If set no empty value is allowed.', false, false);
$this->registerArgument('prioritizedCountries', 'array', 'A list of country codes which should be listed on top of the list.', false, []);
$this->registerArgument('alternativeLanguage', 'string', 'If specified, the country list will be shown in the given language.');
}
public function render(): string
{
if ($this->arguments['required']) {
$this->tag->addAttribute('required', 'required');
}
$name = $this->getName();
if ($this->arguments['multiple']) {
$this->tag->addAttribute('multiple', 'multiple');
$name .= '[]';
}
$this->addAdditionalIdentityPropertiesIfNeeded();
$this->setErrorClassAttribute();
$this->registerFieldNameForFormTokenGeneration($name);
$this->setRespectSubmittedDataValue(true);
$this->tag->addAttribute('name', $name);
$validCountries = $this->getCountryList();
$options = $this->createOptions($validCountries);
$selectedValue = $this->getValueAttribute();
$tagContent = $this->renderPrependOptionTag();
foreach ($options as $value => $label) {
$tagContent .= $this->renderOptionTag($value, $label, $value === $selectedValue);
}
$this->tag->forceClosingTag(true);
$this->tag->setContent($tagContent);
return $this->tag->render();
}
/**
* @param Country[] $countries
* @return array<string, string>
*/
private function createOptions(array $countries): array
{
$options = [];
foreach ($countries as $code => $country) {
switch ($this->arguments['optionLabelField']) {
case 'localizedName':
$options[$code] = $this->translate($country->getLocalizedNameLabel());
break;
case 'name':
$options[$code] = $country->getName();
break;
case 'officialName':
$options[$code] = $country->getOfficialName();
break;
case 'localizedOfficialName':
$name = $this->translate($country->getLocalizedOfficialNameLabel());
if (!$name) {
$name = $this->translate($country->getLocalizedNameLabel());
}
$options[$code] = $name;
break;
default:
throw new InvalidArgumentValueException('Argument "optionLabelField" of <f:form.countrySelect> must either be set to "localizedName", "name", "officialName", or "localizedOfficialName".', 1674076708);
}
}
if ($this->arguments['sortByOptionLabel']) {
asort($options, SORT_LOCALE_STRING);
} else {
ksort($options, SORT_NATURAL);
}
if (($this->arguments['prioritizedCountries'] ?? []) !== []) {
$finalOptions = [];
foreach ($this->arguments['prioritizedCountries'] as $countryCode) {
if (isset($options[$countryCode])) {
$label = $options[$countryCode];
$finalOptions[$countryCode] = $label;
unset($options[$countryCode]);
}
}
foreach ($options as $countryCode => $label) {
$finalOptions[$countryCode] = $label;
}
$options = $finalOptions;
}
return $options;
}
private function translate(string $label): string
{
if ($this->arguments['alternativeLanguage']) {
return (string)LocalizationUtility::translate($label, null, null, $this->arguments['alternativeLanguage']);
}
return (string)LocalizationUtility::translate($label);
}
/**
* Render prepended option tag
*/
private function renderPrependOptionTag(): string
{
if ($this->hasArgument('prependOptionLabel')) {
$value = $this->hasArgument('prependOptionValue') ? $this->arguments['prependOptionValue'] : '';
$label = $this->arguments['prependOptionLabel'];
return $this->renderOptionTag((string)$value, (string)$label, false) . LF;
}
return '';
}
/**
* Render one option tag
*
* @param string $value value attribute of the option tag (will be escaped)
* @param string $label content of the option tag (will be escaped)
* @param bool $isSelected specifies whether to add selected attribute
* @return string the rendered option tag
*/
private function renderOptionTag(string $value, string $label, bool $isSelected): string
{
$output = '<option value="' . htmlspecialchars($value) . '"';
if ($isSelected) {
$output .= ' selected="selected"';
}
if (($this->arguments['prioritizedCountries'] ?? []) !== []
&& in_array($value, $this->arguments['prioritizedCountries'], true)
) {
$output .= ' data-prioritized="1"';
}
$output .= '>' . htmlspecialchars($label) . '</option>';
return $output;
}
/**
* @return Country[]
*/
private function getCountryList(): array
{
$filter = new CountryFilter();
$filter->setOnlyCountries($this->arguments['onlyCountries'] ?? [])
->setExcludeCountries($this->arguments['excludeCountries'] ?? []);
return $this->countryProvider->getFiltered($filter);
}
/**
* Converts an arbitrary value to a plain value.
* Evaluates possible direct "Country" type properties.
*
* @param mixed $value The value to convert
* @return mixed
*/
protected function convertToPlainValue($value)
{
if ($value instanceof Country) {
return $value->getAlpha2IsoCode();
}
return parent::convertToPlainValue($value);
}
}
@@ -0,0 +1,62 @@
<?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\Fluid\ViewHelpers\Form;
/**
* ViewHelper which renders an `<input type="hidden" ...>` tag.
*
* ```
* <f:form.hidden name="myHiddenValue" value="42" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-hidden
*/
final class HiddenViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument(
'respectSubmittedDataValue',
'bool',
'enable or disable the usage of the submitted values',
false,
true
);
}
public function render(): string
{
$name = $this->getName();
$this->registerFieldNameForFormTokenGeneration($name);
$this->setRespectSubmittedDataValue($this->arguments['respectSubmittedDataValue']);
$this->tag->addAttribute('type', 'hidden');
$this->tag->addAttribute('name', $name);
$this->tag->addAttribute('value', (string)$this->getValueAttribute());
$this->addAdditionalIdentityPropertiesIfNeeded();
return $this->tag->render();
}
}
@@ -0,0 +1,64 @@
<?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\Fluid\ViewHelpers\Form;
/**
* ViewHelper which renders a simple password text box `<input type="password">`.
*
* ```
* <f:form.password name="myPassword" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-password
*/
final class PasswordViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
$this->registerArgument(
'respectSubmittedDataValue',
'bool',
'If set to false (default), any user-submitted data is not displayed in the output. If set to true, the password is emitted as clear text in the response. This is not recommended from a security point of view.',
false,
false
);
}
public function render(): string
{
$name = $this->getName();
$this->registerFieldNameForFormTokenGeneration($name);
$this->setRespectSubmittedDataValue($this->arguments['respectSubmittedDataValue']);
$this->tag->addAttribute('type', 'password');
$this->tag->addAttribute('name', $name);
$this->tag->addAttribute('value', (string)$this->getValueAttribute());
$this->addAdditionalIdentityPropertiesIfNeeded();
$this->setErrorClassAttribute();
return $this->tag->render();
}
}
@@ -0,0 +1,79 @@
<?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\Fluid\ViewHelpers\Form;
/**
* ViewHelper which renders a simple radio button `<input type="radio">`.
*
* ```
* <f:form.radio name="myRadioButton" value="someValue" />
* <f:form.radio property="newsletter" value="1" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-radio
*/
final class RadioViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
$this->registerArgument('checked', 'bool', 'Specifies that the input element should be preselected');
$this->registerArgument('value', 'string', 'Value of input tag. Required for radio buttons', true);
}
public function render(): string
{
$checked = $this->arguments['checked'];
$this->tag->addAttribute('type', 'radio');
$nameAttribute = $this->getName();
$valueAttribute = $this->getValueAttribute();
$propertyValue = null;
if ($this->hasMappingErrorOccurred()) {
$propertyValue = $this->getLastSubmittedFormData();
}
if ($checked === null && $propertyValue === null) {
$propertyValue = $this->getPropertyValue();
$propertyValue = $this->convertToPlainValue($propertyValue);
}
if ($propertyValue !== null) {
// no type-safe comparison by intention
$checked = $propertyValue == $valueAttribute;
}
$this->registerFieldNameForFormTokenGeneration($nameAttribute);
$this->tag->addAttribute('name', $nameAttribute);
$this->tag->addAttribute('value', (string)$valueAttribute);
if ($checked === true) {
$this->tag->addAttribute('checked', 'checked');
}
$this->setErrorClassAttribute();
return $this->tag->render();
}
}
@@ -0,0 +1,63 @@
<?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\Fluid\ViewHelpers\Form\Select;
use TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper;
/**
* ViewHelper for adding custom `<optgroup>` tags inside a `<f:form.select>`,
* supports further child `<f:form.select.option>` tags.
*
* ```
* <f:form.select name="mySelect">
* <f:form.select.option value="1">Option one</f:form.select.option>
* <f:form.select.optgroup>
* <f:form.select.option value="3">Grouped option one</f:form.select.option>
* <f:form.select.option value="4">Grouped option two</f:form.select.option>
* </f:form.select.optgroup>
* </f:form.select>>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-select-optgroup
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-select-option
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-select
*/
final class OptgroupViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'optgroup';
public function initializeArguments(): void
{
$this->registerArgument('additionalAttributes', 'array', 'Additional tag attributes. They will be added directly to the resulting HTML tag.');
$this->registerArgument('data', 'array', 'Additional data-* attributes. They will each be added with a "data-" prefix.');
$this->registerArgument('disabled', 'boolean', 'If true, option group is rendered as disabled', false, false);
}
public function render(): string
{
if ($this->arguments['disabled']) {
$this->tag->addAttribute('disabled', 'disabled');
}
$this->tag->setContent($this->renderChildren());
return $this->tag->render();
}
}
@@ -0,0 +1,87 @@
<?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\Fluid\ViewHelpers\Form\Select;
use TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper;
use TYPO3\CMS\Fluid\ViewHelpers\Form\SelectViewHelper;
/**
* ViewHelper for adding custom `<option>` tags inside a `<f:form.select>`.
*
* ```
* <f:form.select name="mySelect">
* <f:form.select.option value="1">Option one</f:form.select.option>
* <f:form.select.optgroup>
* <f:form.select.option value="3">Grouped option one</f:form.select.option>
* <f:form.select.option value="4">Grouped option two</f:form.select.option>
* </f:form.select.optgroup>
* </f:form.select>>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-select-option
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-select
*/
final class OptionViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'option';
public function initializeArguments(): void
{
$this->registerArgument('selected', 'boolean', 'If set, overrides automatic detection of selected state for this option.');
$this->registerArgument('additionalAttributes', 'array', 'Additional tag attributes. They will be added directly to the resulting HTML tag.');
$this->registerArgument('data', 'array', 'Additional data-* attributes. They will each be added with a "data-" prefix.');
$this->registerArgument('value', 'mixed', 'Value to be inserted in HTML tag - must be convertible to string!');
}
public function render(): string
{
$childContent = $this->renderChildren();
$this->tag->setContent((string)$childContent);
$value = $this->arguments['value'] ?? $childContent;
if ($this->arguments['selected'] ?? $this->isValueSelected((string)$value)) {
$this->tag->addAttribute('selected', 'selected');
}
$this->tag->addAttribute('value', (string)$value);
$parentRequestedFormTokenFieldName = $this->renderingContext->getViewHelperVariableContainer()->get(
SelectViewHelper::class,
'registerFieldNameForFormTokenGeneration'
);
if ($parentRequestedFormTokenFieldName) {
// parent (select field) has requested this option must add one more
// entry in the token generation registry for one additional potential
// value of the field. Happens when "multiple" is true on parent.
$this->registerFieldNameForFormTokenGeneration($parentRequestedFormTokenFieldName);
}
return $this->tag->render();
}
private function isValueSelected(string $value): bool
{
$selectedValue = $this->renderingContext->getViewHelperVariableContainer()->get(SelectViewHelper::class, 'selectedValue');
if (is_array($selectedValue)) {
return in_array($value, array_map(strval(...), $selectedValue), true);
}
if ($selectedValue instanceof \Iterator) {
return in_array($value, array_map(strval(...), iterator_to_array($selectedValue)), true);
}
return $value === (string)$selectedValue;
}
}
@@ -0,0 +1,305 @@
<?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\Fluid\ViewHelpers\Form;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
use TYPO3Fluid\Fluid\Core\ViewHelper\MissingArgumentException;
/**
* ViewHelper which renders a `<select>` dropdown list for use within a form.
*
* ```
* <f:form.select name="paymentOptions" options="{payPal: 'PayPal International Services', visa: 'VISA Card'}" value="visa" />
* <f:form.select property="users" options="{userList}" optionValueField="id" optionLabelField="firstName" multiple="true" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-select
*/
final class SelectViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'select';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('options', 'array', 'Associative array with internal IDs as key, and the values are displayed in the select box. Can be combined with or replaced by child f:form.select.* nodes.');
$this->registerArgument('optionsAfterContent', 'boolean', 'If true, places auto-generated option tags after those rendered in the tag content. If false, automatic options come first.', false, false);
$this->registerArgument('optionValueField', 'string', 'If specified, will call the appropriate getter on each object to determine the value.');
$this->registerArgument('optionLabelField', 'string', 'If specified, will call the appropriate getter on each object to determine the label.');
$this->registerArgument('sortByOptionLabel', 'boolean', 'If true, List will be sorted by label.', false, false);
$this->registerArgument('selectAllByDefault', 'boolean', 'If specified options are selected if none was set before.', false, false);
$this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
$this->registerArgument('prependOptionLabel', 'string', 'If specified, will provide an option at first position with the specified label.');
$this->registerArgument('prependOptionValue', 'string', 'If specified, will provide an option at first position with the specified value.');
$this->registerArgument('multiple', 'boolean', 'If set multiple options may be selected.', false, false);
$this->registerArgument('required', 'boolean', 'If set no empty value is allowed.', false, false);
}
public function render(): string
{
if ($this->arguments['required']) {
$this->tag->addAttribute('required', 'required');
}
$name = $this->getName();
if ($this->arguments['multiple']) {
$this->tag->addAttribute('multiple', 'multiple');
$name .= '[]';
}
$this->tag->addAttribute('name', $name);
$options = $this->getOptions();
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
$this->addAdditionalIdentityPropertiesIfNeeded();
$this->setErrorClassAttribute();
$content = '';
// register field name for token generation.
$this->registerFieldNameForFormTokenGeneration($name);
// in case it is a multi-select, we need to register the field name
// as often as there are elements in the box
if ($this->arguments['multiple']) {
$content .= $this->renderHiddenFieldForEmptyValue();
// Register the field name additional times as required by the total number of
// options. Since we already registered it once above, we start the counter at 1
// instead of 0.
$optionsCount = count($options);
for ($i = 1; $i < $optionsCount; $i++) {
$this->registerFieldNameForFormTokenGeneration($name);
}
// save the parent field name so that any child f:form.select.option
// tag will know to call registerFieldNameForFormTokenGeneration
// this is the reason why "self::class" is used instead of static::class (no LSB)
$viewHelperVariableContainer->addOrUpdate(
self::class,
'registerFieldNameForFormTokenGeneration',
$name
);
}
$viewHelperVariableContainer->addOrUpdate(self::class, 'selectedValue', $this->getSelectedValue());
$prependContent = $this->renderPrependOptionTag();
$tagContent = $this->renderOptionTags($options);
$childContent = $this->renderChildren();
$viewHelperVariableContainer->remove(self::class, 'selectedValue');
$viewHelperVariableContainer->remove(self::class, 'registerFieldNameForFormTokenGeneration');
if (isset($this->arguments['optionsAfterContent']) && $this->arguments['optionsAfterContent']) {
$tagContent = $childContent . $tagContent;
} else {
$tagContent .= $childContent;
}
$tagContent = $prependContent . $tagContent;
$this->tag->forceClosingTag(true);
$this->tag->setContent($tagContent);
$content .= $this->tag->render();
return $content;
}
/**
* Render prepended option tag
*/
private function renderPrependOptionTag(): string
{
$output = '';
if ($this->hasArgument('prependOptionLabel')) {
$value = $this->hasArgument('prependOptionValue') ? $this->arguments['prependOptionValue'] : '';
$label = $this->arguments['prependOptionLabel'];
$output .= $this->renderOptionTag((string)$value, (string)$label, false) . LF;
}
return $output;
}
/**
* Render the option tags.
*/
private function renderOptionTags(array $options): string
{
$output = '';
foreach ($options as $value => $label) {
$isSelected = $this->isSelected($value);
$output .= $this->renderOptionTag((string)$value, (string)$label, $isSelected) . LF;
}
return $output;
}
/**
* Render the option tags.
*
* @return array An associative array of options, key will be the value of the option tag
*/
private function getOptions(): array
{
if (!is_array($this->arguments['options']) && !$this->arguments['options'] instanceof \Traversable) {
return [];
}
$options = [];
$optionsArgument = $this->arguments['options'];
foreach ($optionsArgument as $key => $value) {
if (!is_object($value) && !is_array($value)) {
$options[$key] = $value;
continue;
}
if (is_array($value)) {
if (!$this->hasArgument('optionValueField')) {
throw new MissingArgumentException('Missing parameter "optionValueField" in SelectViewHelper for array value options.', 1682693720);
}
if (!$this->hasArgument('optionLabelField')) {
throw new MissingArgumentException('Missing parameter "optionLabelField" in SelectViewHelper for array value options.', 1682693721);
}
$key = ObjectAccess::getPropertyPath($value, (string)$this->arguments['optionValueField']);
$value = ObjectAccess::getPropertyPath($value, (string)$this->arguments['optionLabelField']);
$options[$key ?? ''] = $value;
continue;
}
if ($this->hasArgument('optionValueField')) {
$key = ObjectAccess::getPropertyPath($value, $this->arguments['optionValueField']);
if (is_object($key)) {
if (method_exists($key, '__toString')) {
$key = (string)$key;
} else {
throw new InvalidArgumentValueException('Identifying value for object of class "' . get_debug_type($value) . '" was an object.', 1247827428);
}
}
} elseif (!$this->persistenceManager->isNewObject($value)) {
$key = $this->persistenceManager->getIdentifierByObject($value);
} elseif (is_object($value) && method_exists($value, '__toString')) {
$key = (string)$value;
} elseif (is_object($value)) {
throw new InvalidArgumentValueException('No identifying value for object of class "' . get_class($value) . '" found.', 1247826696);
}
if ($this->hasArgument('optionLabelField')) {
$value = ObjectAccess::getPropertyPath($value, $this->arguments['optionLabelField']);
if (is_object($value)) {
if (method_exists($value, '__toString')) {
$value = (string)$value;
} else {
throw new InvalidArgumentValueException('Label value for object of class "' . get_class($value) . '" was an object without a __toString() method.', 1247827553);
}
}
} elseif (is_object($value) && method_exists($value, '__toString')) {
$value = (string)$value;
} elseif (!$this->persistenceManager->isNewObject($value)) {
$value = $this->persistenceManager->getIdentifierByObject($value);
}
$options[$key ?? ''] = $value;
}
if ($this->arguments['sortByOptionLabel']) {
asort($options, SORT_LOCALE_STRING);
}
return $options;
}
/**
* Render the option tags.
*
* @param mixed $value Value to check for
* @return bool True if the value should be marked as selected.
*/
private function isSelected($value): bool
{
$selectedValue = $this->getSelectedValue();
if ($value === $selectedValue || (string)$value === $selectedValue) {
return true;
}
if ($this->hasArgument('multiple')) {
if ($selectedValue === null && $this->arguments['selectAllByDefault'] === true) {
return true;
}
if (is_array($selectedValue) && in_array($value, $selectedValue)) {
return true;
}
}
return false;
}
/**
* Retrieves the selected value(s)
*
* @return mixed value string or an array of strings
*/
private function getSelectedValue()
{
$this->setRespectSubmittedDataValue(true);
$value = $this->getValueAttribute();
if (!is_array($value) && !$value instanceof \Traversable) {
return $this->getOptionValueScalar($value);
}
$selectedValues = [];
foreach ($value as $selectedValueElement) {
$selectedValues[] = $this->getOptionValueScalar($selectedValueElement);
}
return $selectedValues;
}
/**
* Get the option value for an object
*
* @param mixed $valueElement
* @return string @todo: Does not always return string ...
*/
private function getOptionValueScalar($valueElement)
{
if (is_object($valueElement)) {
if ($this->hasArgument('optionValueField')) {
return ObjectAccess::getPropertyPath($valueElement, $this->arguments['optionValueField']);
}
if (!$this->persistenceManager->isNewObject($valueElement)) {
if ($valueElement instanceof DomainObjectInterface) {
// We prefer to use the `getUid()` method because this returns the properly overlaid identifier (defaultLanguageRecordUid).
// Otherwise, an identifier would contain '[defaultLanguageRecordUid]_[localizedRecordUid]'. This in turn
// will not properly trigger the select option "is selected" comparison.
// @see AbstractFormFieldViewHelper->convertToPlainValue()
return $valueElement->getUid() ?? $this->persistenceManager->getIdentifierByObject($valueElement);
}
return $this->persistenceManager->getIdentifierByObject($valueElement);
}
if ($valueElement instanceof \BackedEnum) {
return $valueElement->value;
}
if ($valueElement instanceof \UnitEnum) {
return $valueElement->name;
}
return (string)$valueElement;
}
return $valueElement;
}
/**
* Render one option tag
*
* @param string $value value attribute of the option tag (will be escaped)
* @param string $label content of the option tag (will be escaped)
* @param bool $isSelected specifies whether to add selected attribute
* @return string the rendered option tag
*/
private function renderOptionTag(string $value, string $label, bool $isSelected): string
{
$output = '<option value="' . htmlspecialchars($value) . '"';
if ($isSelected) {
$output .= ' selected="selected"';
}
$output .= '>' . htmlspecialchars($label) . '</option>';
return $output;
}
}
@@ -0,0 +1,49 @@
<?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\Fluid\ViewHelpers\Form;
/**
* ViewHelper which renders a form submit button.
*
* ```
* <f:form.submit value="Send Mail" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-submit
*/
final class SubmitViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function render(): string
{
$name = $this->getName();
$this->registerFieldNameForFormTokenGeneration($name);
$this->tag->addAttribute('type', 'submit');
$this->tag->addAttribute('value', (string)$this->getValueAttribute());
if (!empty($name)) {
$this->tag->addAttribute('name', $name);
}
return $this->tag->render();
}
}
@@ -0,0 +1,64 @@
<?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\Fluid\ViewHelpers\Form;
/**
* ViewHelper which renders a `<textarea>` large text input area inside a form.
*
* The value of the text area needs to be set via the `value` attribute, as with all other f:form ViewHelpers.
*
* ```
* <f:form.textarea name="myTextArea" value="This is shown inside the textarea" />
* <f:form.textarea property="myProperty" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-textarea
*/
final class TextareaViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'textarea';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
$this->registerArgument('required', 'bool', 'Specifies whether the textarea is required', false, false);
}
public function render(): string
{
$required = $this->arguments['required'];
$name = $this->getName();
$this->registerFieldNameForFormTokenGeneration($name);
$this->setRespectSubmittedDataValue(true);
$this->tag->forceClosingTag(true);
$this->tag->addAttribute('name', $name);
if ($required === true) {
$this->tag->addAttribute('required', 'required');
}
$this->tag->setContent(htmlspecialchars((string)$this->getValueAttribute()));
$this->addAdditionalIdentityPropertiesIfNeeded();
$this->setErrorClassAttribute();
return $this->tag->render();
}
}
@@ -0,0 +1,72 @@
<?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\Fluid\ViewHelpers\Form;
/**
* ViewHelper which renders a text field `<input type="text">`.
*
* ```
* <f:form.textfield name="myTextBox" value="default value" />
* <f:form.textfield property="ownerMail" required="true" type="email" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-textfield
*/
final class TextfieldViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
$this->registerArgument('required', 'bool', 'If the field is required or not', false, false);
$this->registerArgument('type', 'string', 'The field type, e.g. "text", "email", "url" etc.', false, 'text');
}
public function render(): string
{
$required = $this->arguments['required'];
$type = $this->arguments['type'];
$name = $this->getName();
$this->registerFieldNameForFormTokenGeneration($name);
$this->setRespectSubmittedDataValue(true);
$this->tag->addAttribute('type', $type);
$this->tag->addAttribute('name', $name);
$value = $this->getValueAttribute();
if ($value !== null) {
$this->tag->addAttribute('value', $value);
}
if ($required !== false) {
$this->tag->addAttribute('required', 'required');
}
$this->addAdditionalIdentityPropertiesIfNeeded();
$this->setErrorClassAttribute();
return $this->tag->render();
}
}
@@ -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\Fluid\ViewHelpers\Form;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
use TYPO3\CMS\Extbase\Service\ExtensionService;
use TYPO3\CMS\Extbase\Service\FileHandlingService;
use TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper which renders a checkbox field used for file upload deletion in Extbase forms.
*
* ```
* <f:form.uploadDeleteCheckbox id="file" property="file" fileReference="{myModel.file}" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-uploaddeletecheckbox
*/
final class UploadDeleteCheckboxViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function __construct(
private readonly HashService $hashService,
private readonly ExtensionService $extensionService,
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('id', 'string', 'ID of the generated checkbox element');
$this->registerArgument('property', 'string', 'Name of object property', true);
$this->registerArgument('fileReference', FileReference::class, 'The file reference object', true);
}
public function render(): string
{
/** @var ?FileReference $fileReference */
$fileReference = $this->arguments['fileReference'];
$property = $this->arguments['property'];
$idAttribute = $this->arguments['id'] ?? '';
// Early return, if no file reference given
if (!$fileReference instanceof FileReference) {
return '';
}
$this->tag->addAttribute('type', 'checkbox');
$request = ($this->renderingContext->getAttribute(ServerRequestInterface::class));
$extbaseRequestParams = $request->getAttribute('extbase');
$extensionName = $extbaseRequestParams->getControllerExtensionName();
$pluginName = $extbaseRequestParams->getPluginName();
if ($extensionName === '' || $pluginName === '') {
throw new \RuntimeException('ExtensionName or PluginName not set in Extbase request', 1719660837);
}
$deleteData = [
'property' => $property,
'fileReference' => $fileReference->getUid(),
];
$pluginNamespace = $this->extensionService->getPluginNamespace($extensionName, $pluginName);
$formObjectName = $this->getFormObjectName();
$fileReferenceIdentifier = $this->hashService->hmac($property . $fileReference->getUid(), self::class);
$nameAttribute = $pluginNamespace . '[' . FileHandlingService::DELETE_IDENTIFIER . ']'
. '[' . $formObjectName . ']' . '[' . $fileReferenceIdentifier . ']';
$valueAttribute = $this->hashService->appendHmac(
json_encode($deleteData, JSON_THROW_ON_ERROR),
FileHandlingService::DELETE_IDENTIFIER
);
$checked = false;
if ($this->hasMappingErrorOccurred($extbaseRequestParams)) {
$checked = $this->getCheckedState($request, $pluginNamespace, $formObjectName, $fileReferenceIdentifier);
}
$this->tag->addAttribute('id', $idAttribute);
$this->tag->addAttribute('name', $nameAttribute);
$this->tag->addAttribute('value', (string)$valueAttribute);
if ($checked === true) {
$this->tag->addAttribute('checked', 'checked');
}
return $this->tag->render();
}
/**
* Returns the boolean checked state for the given identifier evaluated from POST data.
*/
private function getCheckedState(
ServerRequestInterface $request,
string $fieldNamePrefix,
string $formObjectName,
mixed $identifier
): bool {
return (bool)($request->getParsedBody()[$fieldNamePrefix][FileHandlingService::DELETE_IDENTIFIER][$formObjectName][$identifier] ?? false);
}
private function getFormObjectName(): string
{
$formObjectName = $this->renderingContext->getViewHelperVariableContainer()->get(
FormViewHelper::class,
'formObjectName'
);
if (empty($formObjectName)) {
throw new \RuntimeException('UploadDeleteCheckboxViewHelper can only be used on Fluid form context', 1719655880);
}
return $formObjectName;
}
/**
* Checks if a property mapping error has occurred in the last request.
*/
private function hasMappingErrorOccurred(ExtbaseRequestParameters $extbaseRequest): bool
{
return $extbaseRequest->getOriginalRequest() !== null;
}
}
@@ -0,0 +1,67 @@
<?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\Fluid\ViewHelpers\Form;
/**
* ViewHelper which renders an `<input type="file">` file upload HTML form element.
* Make sure to set the `enctype="multipart/form-data"` attribute on the surrounding form!
*
* ```
* <f:form.upload name="file" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-upload
*/
final class UploadViewHelper extends AbstractFormFieldViewHelper
{
/**
* @var string
*/
protected $tagName = 'input';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
}
public function render(): string
{
$multiple = isset($this->additionalArguments['multiple']);
$name = $this->getName();
$allowedFields = ['name', 'type', 'tmp_name', 'error', 'size'];
foreach ($allowedFields as $fieldName) {
if ($multiple) {
$formTokenFieldName = sprintf('%s[*][%s]', $name, $fieldName);
} else {
$formTokenFieldName = $name . '[' . $fieldName . ']';
}
$this->registerFieldNameForFormTokenGeneration($formTokenFieldName);
}
$this->tag->addAttribute('type', 'file');
if ($multiple) {
$this->tag->addAttribute('name', $name . '[]');
} else {
$this->tag->addAttribute('name', $name);
}
$this->setErrorClassAttribute();
return $this->tag->render();
}
}
@@ -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!
*/
namespace TYPO3\CMS\Fluid\ViewHelpers\Form;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3Fluid\Fluid\Core\Variables\ScopedVariableProvider;
use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper which renders form validation results.
*
* ```
* <f:form.validationResults>
* <f:if condition="{validationResults.flattenedErrors}">
* <ul>
* <f:for each="{validationResults.flattenedErrors}" as="errors" key="propertyPath">
* <li>{propertyPath}
* <ul>
* <f:for each="{errors}" as="error">
* <li>{error.code}: {error}</li>
* </f:for>
* </ul>
* </li>
* </f:for>
* </ul>
* </f:if>
* </f:form.validationResults>
*
* <f:form.validationResults for="someProperty">
* <f:for each="{validationResults.flattenedErrors}" as="errors" key="propertyPath">
* <f:for each="{errors}" as="error">
* <p data-property-path="{propertyPath}">{error.code}: {error}</p>
* </f:for>
* </f:for>
* </f:form.validationResults>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form-validationresults
*/
final class ValidationResultsViewHelper extends AbstractViewHelper
{
/**
* As this ViewHelper renders HTML, the output must not be escaped.
*
* @var bool
*/
protected $escapeOutput = false;
public function initializeArguments(): void
{
$this->registerArgument('for', 'string', 'The name of the error name (e.g. argument name or property name). This can also be a property path (like blog.title), and will then only display the validation errors of that property.', false, '');
$this->registerArgument('as', 'string', 'The name of the variable to store the current error', false, 'validationResults');
}
public function render(): string
{
$for = $this->arguments['for'];
$as = $this->arguments['as'];
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)
|| !$this->renderingContext->getAttribute(ServerRequestInterface::class) instanceof RequestInterface
) {
throw new \RuntimeException('ValidationResultsViewHelper needs an extbase request to work.', 1724244193);
}
$extbaseRequestParameters = $this->renderingContext->getAttribute(ServerRequestInterface::class)->getAttribute('extbase');
$validationResults = $extbaseRequestParameters->getOriginalRequestMappingResults();
if ($validationResults !== null && $for !== '') {
$validationResults = $validationResults->forProperty($for);
}
$variableProvider = new ScopedVariableProvider($this->renderingContext->getVariableProvider(), new StandardVariableProvider([$as => $validationResults]));
$this->renderingContext->setVariableProvider($variableProvider);
$output = (string)$this->renderChildren();
$this->renderingContext->setVariableProvider($variableProvider->getGlobalVariableProvider());
return $output;
}
}
+467
View File
@@ -0,0 +1,467 @@
<?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\Fluid\ViewHelpers;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\SecurityAspect;
use TYPO3\CMS\Core\Crypto\HashAlgo;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Security\RequestToken;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3\CMS\Extbase\Security\HashScope;
use TYPO3\CMS\Extbase\Service\ExtensionService;
use TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormViewHelper;
use TYPO3\CMS\Fluid\ViewHelpers\Form\CheckboxViewHelper;
/**
* ViewHelper to generate a `<form>` tag and prepare context for
* further `<f:form>` ViewHelpers within that form.
* Tailored for Extbase plugins, uses Extbase Request.
*
* ```
* <f:form action="new" controller="BlogPostEditing" object="{blog}" name="blog" method="post"
* arguments="{somePostKey: 'someValue'}" enctype="multipart/form-data">...</f:form>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-form
*/
class FormViewHelper extends AbstractFormViewHelper
{
/**
* @var string
*/
protected $tagName = 'form';
protected HashService $hashService;
protected MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService;
protected ExtensionService $extensionService;
protected ConfigurationManagerInterface $configurationManager;
/**
* We need the arguments of the formActionUri on request hash calculation
* therefore we will store them in here right after calling uriBuilder
*/
protected array $formActionUriArguments = [];
public function injectHashService(HashService $hashService): void
{
$this->hashService = $hashService;
}
public function injectMvcPropertyMappingConfigurationService(MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService): void
{
$this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
}
public function injectExtensionService(ExtensionService $extensionService): void
{
$this->extensionService = $extensionService;
}
public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
{
$this->configurationManager = $configurationManager;
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('action', 'string', 'Target action');
$this->registerArgument('arguments', 'array', 'Arguments (do not use reserved keywords "action", "controller" or "format" if not referring to these internal variables specifically)', false, []);
$this->registerArgument('controller', 'string', 'Target controller');
$this->registerArgument('extensionName', 'string', 'Target Extension Name (without `tx_` prefix and no underscores). If NULL the current extension name is used');
$this->registerArgument('pluginName', 'string', 'Target plugin. If empty, the current plugin name is used');
$this->registerArgument('pageUid', 'int', 'Target page uid');
$this->registerArgument('object', 'mixed', 'Object to use for the form. Use in conjunction with the "property" attribute on the sub tags');
$this->registerArgument('pageType', 'int', 'Target page type', false, 0);
$this->registerArgument('noCache', 'bool', 'set this to disable caching for the target page. You should not need this.', false, false);
$this->registerArgument('section', 'string', 'The anchor to be added to the action URI (only active if $actionUri is not set)', false, '');
$this->registerArgument('format', 'string', 'The requested format (e.g. ".html") of the target page (only active if $actionUri is not set)', false, '');
$this->registerArgument('additionalParams', 'array', 'additional action URI query parameters that won\'t be prefixed like $arguments (overrule $arguments) (only active if $actionUri is not set)', false, []);
$this->registerArgument('absolute', 'bool', 'If set, an absolute action URI is rendered (only active if $actionUri is not set)', false, false);
$this->registerArgument('addQueryString', 'string', 'If set, the current query parameters will be kept in the URL. If set to "untrusted", then ALL query parameters will be added. Be aware, that this might lead to problems when the generated link is cached.', false, false);
$this->registerArgument('argumentsToBeExcludedFromQueryString', 'array', 'arguments to be removed from the action URI. Only active if $addQueryString = TRUE and $actionUri is not set', false, []);
$this->registerArgument('fieldNamePrefix', 'string', 'Prefix that will be added to all field names within this form. If not set the prefix will be tx_yourExtension_plugin');
$this->registerArgument('actionUri', 'string', 'can be used to overwrite the "action" attribute of the form tag');
$this->registerArgument('objectName', 'string', 'name of the object that is bound to this form. If this argument is not specified, the name attribute of this form is used to determine the FormObjectName');
$this->registerArgument('hiddenFieldClassName', 'string', 'hiddenFieldClassName');
$this->registerArgument('requestToken', 'mixed', 'whether to add that request token to the form');
$this->registerArgument('signingType', 'string', 'which signing type to be used on the request token (falls back to "nonce")');
$this->registerArgument('method', 'string', 'Transfer type (get or post)', false, 'post');
$this->registerArgument('name', 'string', 'Name of form');
$this->registerArgument('novalidate', 'bool', 'Indicate that the form is not to be validated on submit.');
}
public function render(): string
{
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)
|| !$this->renderingContext->getAttribute(ServerRequestInterface::class) instanceof RequestInterface) {
throw new \RuntimeException(
'ViewHelper f:form can be used only in extbase context and needs a request implementing extbase RequestInterface.',
1639821904
);
}
$this->setFormActionUri();
// Force 'method="get"' or 'method="post"', defaulting to "post".
if (isset($this->arguments['method']) && strtolower($this->arguments['method']) === 'get') {
$this->tag->addAttribute('method', 'get');
} else {
$this->tag->addAttribute('method', 'post');
}
if (!empty($this->arguments['name'])) {
$this->tag->addAttribute('name', $this->arguments['name']);
}
if (isset($this->arguments['novalidate']) && $this->arguments['novalidate'] === true) {
$this->tag->addAttribute('novalidate', 'novalidate');
}
$this->addFormObjectNameToViewHelperVariableContainer();
$this->addFormObjectToViewHelperVariableContainer();
$this->addFieldNamePrefixToViewHelperVariableContainer();
$this->addFormFieldNamesToViewHelperVariableContainer();
$formContent = $this->renderChildren();
if (isset($this->arguments['hiddenFieldClassName']) && $this->arguments['hiddenFieldClassName'] !== null) {
$content = LF . '<div class="' . htmlspecialchars($this->arguments['hiddenFieldClassName']) . '">';
} else {
$content = LF . '<div>';
}
$content .= $this->renderHiddenIdentityField($this->arguments['object'] ?? null, $this->getFormObjectName());
$content .= $this->renderAdditionalIdentityFields();
$content .= $this->renderHiddenReferrerFields();
$content .= $this->renderRequestTokenHiddenField();
// Render the trusted list of all properties after everything else has been rendered
$content .= $this->renderTrustedPropertiesField();
$content .= LF . '</div>' . LF;
$content .= $formContent;
$this->tag->setContent($content);
$this->removeFieldNamePrefixFromViewHelperVariableContainer();
$this->removeFormObjectFromViewHelperVariableContainer();
$this->removeFormObjectNameFromViewHelperVariableContainer();
$this->removeFormFieldNamesFromViewHelperVariableContainer();
$this->removeCheckboxFieldNamesFromViewHelperVariableContainer();
return $this->tag->render();
}
/**
* Sets the "action" attribute of the form tag
*/
protected function setFormActionUri(): void
{
if ($this->hasArgument('actionUri')) {
$formActionUri = $this->arguments['actionUri'];
} else {
/** @var RequestInterface $request */
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$uriBuilder
->reset()
->setRequest($request)
->setTargetPageType((int)($this->arguments['pageType'] ?? 0))
->setNoCache((bool)($this->arguments['noCache'] ?? false))
->setSection($this->arguments['section'] ?? '')
->setCreateAbsoluteUri((bool)($this->arguments['absolute'] ?? false))
->setArguments(isset($this->arguments['additionalParams']) ? (array)$this->arguments['additionalParams'] : [])
->setAddQueryString($this->arguments['addQueryString'] ?? false)
->setArgumentsToBeExcludedFromQueryString(isset($this->arguments['argumentsToBeExcludedFromQueryString']) ? (array)$this->arguments['argumentsToBeExcludedFromQueryString'] : [])
->setFormat($this->arguments['format'] ?? '')
;
$pageUid = (int)($this->arguments['pageUid'] ?? 0);
if ($pageUid > 0) {
$uriBuilder->setTargetPageUid($pageUid);
}
$formActionUri = $uriBuilder->uriFor(
$this->arguments['action'] ?? null,
$this->arguments['arguments'] ?? [],
$this->arguments['controller'] ?? null,
$this->arguments['extensionName'] ?? null,
$this->arguments['pluginName'] ?? null
);
$this->formActionUriArguments = $uriBuilder->getArguments();
}
$this->tag->addAttribute('action', $formActionUri);
}
/**
* Render additional identity fields which were registered by form elements.
* This happens if a form field is defined like property="bla.blubb" - then we might need an identity property for the sub-object "bla".
*
* @return string HTML-string for the additional identity properties
*/
protected function renderAdditionalIdentityFields(): string
{
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if ($viewHelperVariableContainer->exists(FormViewHelper::class, 'additionalIdentityProperties')) {
$additionalIdentityProperties = $viewHelperVariableContainer->get(FormViewHelper::class, 'additionalIdentityProperties');
$output = '';
foreach ($additionalIdentityProperties as $identity) {
$output .= LF . $identity;
}
return $output;
}
return '';
}
/**
* Renders hidden form fields for referrer information about
* the current controller and action.
*
* @return string Hidden fields with referrer information
* @todo filter out referrer information that is equal to the target (e.g. same packageKey)
*/
protected function renderHiddenReferrerFields(): string
{
/** @var RequestInterface $request */
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$extensionName = $request->getControllerExtensionName();
$controllerName = $request->getControllerName();
$actionName = $request->getControllerActionName();
$actionRequest = [
'@extension' => $extensionName,
'@controller' => $controllerName,
'@action' => $actionName,
];
$endingSlash = ($this->shouldUseXHtmlSlash() ? '/' : '');
$result = LF;
$result .= '<input type="hidden" name="' . htmlspecialchars($this->prefixFieldName('__referrer[@extension]')) . '" value="' . htmlspecialchars($extensionName) . '" ' . $endingSlash . '>' . LF;
$result .= '<input type="hidden" name="' . htmlspecialchars($this->prefixFieldName('__referrer[@controller]')) . '" value="' . htmlspecialchars($controllerName) . '" ' . $endingSlash . '>' . LF;
$result .= '<input type="hidden" name="' . htmlspecialchars($this->prefixFieldName('__referrer[@action]')) . '" value="' . htmlspecialchars($actionName) . '" ' . $endingSlash . '>' . LF;
$result .= '<input type="hidden" name="' . htmlspecialchars($this->prefixFieldName('__referrer[arguments]')) . '" value="' . htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($request->getArguments())), HashScope::ReferringArguments->prefix(), HashAlgo::SHA3_256)) . '" ' . $endingSlash . '>' . LF;
$result .= '<input type="hidden" name="' . htmlspecialchars($this->prefixFieldName('__referrer[@request]')) . '" value="' . htmlspecialchars($this->hashService->appendHmac(json_encode($actionRequest), HashScope::ReferringRequest->prefix(), HashAlgo::SHA3_256)) . '" ' . $endingSlash . '>' . LF;
return $result;
}
/**
* Adds the form object name to the ViewHelperVariableContainer if "objectName" argument or "name" attribute is specified.
*/
protected function addFormObjectNameToViewHelperVariableContainer(): void
{
$formObjectName = $this->getFormObjectName();
if ($formObjectName !== null) {
$this->renderingContext->getViewHelperVariableContainer()->add(FormViewHelper::class, 'formObjectName', $formObjectName);
}
}
/**
* Removes the form name from the ViewHelperVariableContainer.
*/
protected function removeFormObjectNameFromViewHelperVariableContainer(): void
{
$formObjectName = $this->getFormObjectName();
if ($formObjectName !== null) {
$this->renderingContext->getViewHelperVariableContainer()->remove(FormViewHelper::class, 'formObjectName');
}
}
/**
* Returns the name of the object that is bound to this form.
* If the "objectName" argument has been specified, this is returned. Otherwise the name attribute of this form.
* If neither objectName nor name arguments have been set, NULL is returned.
*
* @return string specified Form name or NULL if neither $objectName nor $name arguments have been specified
*/
protected function getFormObjectName(): ?string
{
$formObjectName = null;
if ($this->hasArgument('objectName')) {
$formObjectName = $this->arguments['objectName'];
} elseif ($this->hasArgument('name')) {
$formObjectName = $this->arguments['name'];
}
return $formObjectName;
}
/**
* Adds the object that is bound to this form to the ViewHelperVariableContainer if the formObject attribute is specified.
*/
protected function addFormObjectToViewHelperVariableContainer(): void
{
if ($this->hasArgument('object')) {
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
$viewHelperVariableContainer->add(FormViewHelper::class, 'formObject', $this->arguments['object']);
$viewHelperVariableContainer->add(FormViewHelper::class, 'additionalIdentityProperties', []);
}
}
/**
* Removes the form object from the ViewHelperVariableContainer.
*/
protected function removeFormObjectFromViewHelperVariableContainer(): void
{
if ($this->hasArgument('object')) {
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
$viewHelperVariableContainer->remove(FormViewHelper::class, 'formObject');
$viewHelperVariableContainer->remove(FormViewHelper::class, 'additionalIdentityProperties');
}
}
/**
* Adds the field name prefix to the ViewHelperVariableContainer.
*/
protected function addFieldNamePrefixToViewHelperVariableContainer(): void
{
$fieldNamePrefix = $this->getFieldNamePrefix();
$this->renderingContext->getViewHelperVariableContainer()->add(FormViewHelper::class, 'fieldNamePrefix', $fieldNamePrefix);
}
protected function getFieldNamePrefix(): string
{
if ($this->hasArgument('fieldNamePrefix')) {
return $this->arguments['fieldNamePrefix'];
}
return $this->getDefaultFieldNamePrefix();
}
/**
* Removes field name prefix from the ViewHelperVariableContainer.
*/
protected function removeFieldNamePrefixFromViewHelperVariableContainer(): void
{
$this->renderingContext->getViewHelperVariableContainer()->remove(FormViewHelper::class, 'fieldNamePrefix');
}
/**
* Adds a container for form field names to the ViewHelperVariableContainer.
*/
protected function addFormFieldNamesToViewHelperVariableContainer(): void
{
$this->renderingContext->getViewHelperVariableContainer()->add(FormViewHelper::class, 'formFieldNames', []);
}
/**
* Removes the container for form field names from the ViewHelperVariableContainer.
*/
protected function removeFormFieldNamesFromViewHelperVariableContainer(): void
{
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
$viewHelperVariableContainer->remove(FormViewHelper::class, 'formFieldNames');
if ($viewHelperVariableContainer->exists(FormViewHelper::class, 'renderedHiddenFields')) {
$viewHelperVariableContainer->remove(FormViewHelper::class, 'renderedHiddenFields');
}
}
/**
* Retrieves the default field name prefix for this form
*/
protected function getDefaultFieldNamePrefix(): string
{
/** @var RequestInterface $request */
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
if ($request->getAttribute('applicationType') && ApplicationType::fromRequest($request)->isBackend()) {
// Backend URLs do not have a prefix
return '';
}
if ($this->hasArgument('extensionName')) {
$extensionName = $this->arguments['extensionName'];
} else {
$extensionName = $request->getControllerExtensionName();
}
if ($this->hasArgument('pluginName')) {
$pluginName = $this->arguments['pluginName'];
} else {
$pluginName = $request->getPluginName();
}
if ($extensionName !== null && $pluginName != null) {
return $this->extensionService->getPluginNamespace($extensionName, $pluginName);
}
return '';
}
/**
* Remove Checkbox field names from ViewHelper variable container, to start from scratch when a new form starts.
*/
protected function removeCheckboxFieldNamesFromViewHelperVariableContainer(): void
{
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if ($viewHelperVariableContainer->exists(CheckboxViewHelper::class, 'checkboxFieldNames')) {
$viewHelperVariableContainer->remove(CheckboxViewHelper::class, 'checkboxFieldNames');
}
}
/**
* Render the request hash field
*/
protected function renderTrustedPropertiesField(): string
{
$formFieldNames = $this->renderingContext->getViewHelperVariableContainer()->get(FormViewHelper::class, 'formFieldNames');
$requestHash = $this->mvcPropertyMappingConfigurationService->generateTrustedPropertiesToken($formFieldNames, $this->getFieldNamePrefix());
return '<input type="hidden" name="' . htmlspecialchars($this->prefixFieldName('__trustedProperties')) . '" value="' . htmlspecialchars($requestHash) . '" ' . ($this->shouldUseXHtmlSlash() ? '/' : '') . '>';
}
protected function renderRequestTokenHiddenField(): string
{
$requestToken = $this->arguments['requestToken'] ?? null;
$signingType = $this->arguments['signingType'] ?? null;
$isTrulyRequestToken = is_int($requestToken) && $requestToken === 1
|| is_string($requestToken) && strtolower($requestToken) === 'true';
$formAction = $this->tag->getAttribute('action');
// basically "request token, yes" - uses form-action URI as scope
if ($isTrulyRequestToken || $requestToken === '@nonce') {
$requestToken = RequestToken::create($formAction);
} elseif (is_string($requestToken) && $requestToken !== '') {
// basically "request token with 'my-scope'" - uses 'my-scope'
$requestToken = RequestToken::create($requestToken);
}
if (!$requestToken instanceof RequestToken) {
return '';
}
if (strtolower((string)($this->arguments['method'] ?? '')) === 'get') {
throw new \LogicException('Cannot apply request token for forms sent via HTTP GET', 1651775963);
}
$context = GeneralUtility::makeInstance(Context::class);
$securityAspect = SecurityAspect::provideIn($context);
// @todo currently defaults to 'nonce', there might be a better strategy in the future
$signingType = $signingType ?: 'nonce';
$signingProvider = $securityAspect->getSigningSecretResolver()->findByType($signingType);
if ($signingProvider === null) {
throw new \LogicException(sprintf('Cannot find request token signing type "%s"', $signingType), 1664260307);
}
$signingSecret = $signingProvider->provideSigningSecret();
$requestToken = $requestToken->withMergedParams(['request' => ['uri' => $formAction]]);
$attrs = [
'type' => 'hidden',
'name' => RequestToken::PARAM_NAME,
'value' => $requestToken->toHashSignedJwt($signingSecret),
];
return '<input ' . GeneralUtility::implodeAttributes($attrs, true) . ($this->shouldUseXHtmlSlash() ? '/' : '') . '>';
}
}
@@ -0,0 +1,43 @@
<?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\Fluid\ViewHelpers\Format;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* This is the base class for ViewHelpers that work with encodings.
* Currently, that are format.htmlentities and format.htmlentitiesDecode
*/
abstract class AbstractEncodingViewHelper extends AbstractViewHelper
{
/**
* @var string
*/
protected static $defaultEncoding;
/**
* Resolve the default encoding. If none is set in Frontend or Backend, uses UTF-8.
*/
protected static function resolveDefaultEncoding(): string
{
if (self::$defaultEncoding === null) {
self::$defaultEncoding = 'UTF-8';
}
return self::$defaultEncoding;
}
}
@@ -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!
*/
namespace TYPO3\CMS\Fluid\ViewHelpers\Format;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper which formats an integer (byte count) into specific human-readable output.
*
* ```
* <f:format.bytes decimals="2" decimalSeparator="." thousandsSeparator=",">{file.size}</f:format.bytes>
* <f:format.bytes decimals="2" decimalSeparator="." thousandsSeparator="," units="KB,MB,GB" value="{file.size}" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-format-bytes
*/
final class BytesViewHelper extends AbstractViewHelper
{
/**
* Output is escaped already. We must not escape children, to avoid double encoding.
*
* @var bool
*/
protected $escapeChildren = false;
public function initializeArguments(): void
{
$this->registerArgument('value', 'int|float|string', 'The incoming data to convert, or NULL if VH children should be used');
$this->registerArgument('decimals', 'int', 'The number of digits after the decimal point', false, 0);
$this->registerArgument('decimalSeparator', 'string', 'The decimal point character', false, '.');
$this->registerArgument('thousandsSeparator', 'string', 'The character for grouping the thousand digits', false, ',');
$this->registerArgument('units', 'string', 'comma separated list of available units, default is LocalizationUtility::translate(\'viewhelper.format.bytes.units\', \'fluid\')');
}
/**
* Render the supplied byte count as a human-readable string.
*/
public function render(): string
{
if ($this->arguments['units'] !== null) {
$units = $this->arguments['units'];
} else {
$units = LocalizationUtility::translate('viewhelper.format.bytes.units', 'fluid');
}
$units = GeneralUtility::trimExplode(',', (string)$units, true);
$value = $this->renderChildren();
if (is_numeric($value)) {
$value = (float)$value;
} else {
$value = 0;
}
$bytes = max($value, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= 2 ** (10 * $pow);
return sprintf(
'%s %s',
number_format(
round($bytes, 4 * $this->arguments['decimals']),
(int)$this->arguments['decimals'],
$this->arguments['decimalSeparator'],
$this->arguments['thousandsSeparator']
),
$units[$pow]
);
}
/**
* Explicitly set argument name to be used as content.
*/
public function getContentArgumentName(): string
{
return 'value';
}
}
@@ -0,0 +1,69 @@
<?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\Fluid\ViewHelpers\Format;
use TYPO3\CMS\Core\Html\HtmlCropper;
use TYPO3\CMS\Core\Text\TextCropper;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper which can crop (shorten) a text.
* Whitespace within the `<f:format.crop>` element will be counted as characters.
*
* ```
* <f:format.crop maxCharacters="10" append="&hellip;[more]">
* This is some very long text
* </f:format.crop>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-format-crop
*/
final class CropViewHelper extends AbstractViewHelper
{
/**
* The output may contain HTML and can not be escaped.
*
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly TextCropper $textCropper,
private readonly HtmlCropper $htmlCropper,
) {}
public function initializeArguments(): void
{
$this->registerArgument('maxCharacters', 'int', 'Place where to truncate the string', true);
$this->registerArgument('append', 'string', 'What to append, if truncation happened', false, '&hellip;');
$this->registerArgument('respectWordBoundaries', 'bool', 'If TRUE and division is in the middle of a word, the remains of that word is removed.', false, true);
$this->registerArgument('respectHtml', 'bool', 'If TRUE the cropped string will respect HTML tags and entities. Technically that means, that cropHTML() is called rather than crop()', false, true);
}
public function render(): string
{
$maxCharacters = (int)$this->arguments['maxCharacters'];
$append = (string)$this->arguments['append'];
$respectWordBoundaries = (bool)($this->arguments['respectWordBoundaries']);
$respectHtml = (bool)$this->arguments['respectHtml'];
$stringToTruncate = (string)$this->renderChildren();
return $respectHtml
? $this->htmlCropper->crop($stringToTruncate, $maxCharacters, $append, $respectWordBoundaries)
: $this->textCropper->crop($stringToTruncate, $maxCharacters, $append, $respectWordBoundaries);
}
}
@@ -0,0 +1,83 @@
<?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\Fluid\ViewHelpers\Format;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper which formats a given float to a currency representation.
*
* ```
* <f:format.currency decimalSeparator="." thousandsSeparator="," decimals="2"
* currencySign="$" prependCurrency="true" separateCurrency="false">
* 54321
* </f:format.currency>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-format-currency
*/
final class CurrencyViewHelper extends AbstractViewHelper
{
/**
* Output is escaped already. We must not escape children, to avoid double encoding.
*
* @var bool
*/
protected $escapeChildren = false;
public function initializeArguments(): void
{
$this->registerArgument('currencySign', 'string', 'The currency sign, eg $ or €.', false, '');
$this->registerArgument('decimalSeparator', 'string', 'The separator for the decimal point.', false, ',');
$this->registerArgument('thousandsSeparator', 'string', 'The thousands separator.', false, '.');
$this->registerArgument('prependCurrency', 'bool', 'Select if the currency sign should be prepended', false, false);
$this->registerArgument('separateCurrency', 'bool', 'Separate the currency sign from the number by a single space, defaults to true due to backwards compatibility', false, true);
$this->registerArgument('decimals', 'int', 'Set decimals places.', false, 2);
$this->registerArgument('useDash', 'bool', 'Use the dash instead of decimal 00', false, false);
}
public function render(): string
{
$currencySign = $this->arguments['currencySign'];
$decimalSeparator = $this->arguments['decimalSeparator'];
$thousandsSeparator = $this->arguments['thousandsSeparator'];
$prependCurrency = $this->arguments['prependCurrency'];
$separateCurrency = $this->arguments['separateCurrency'];
$decimals = (int)$this->arguments['decimals'];
$useDash = $this->arguments['useDash'];
$floatToFormat = $this->renderChildren();
if (empty($floatToFormat)) {
$floatToFormat = 0.0;
} else {
$floatToFormat = (float)$floatToFormat;
}
$output = number_format($floatToFormat, $decimals, $decimalSeparator, $thousandsSeparator);
if ($useDash && $floatToFormat === floor($floatToFormat)) {
$output = explode($decimalSeparator, $output)[0] . $decimalSeparator . '—';
}
if ($currencySign !== '') {
$currencySeparator = $separateCurrency ? ' ' : '';
if ($prependCurrency === true) {
$output = $currencySign . $currencySeparator . $output;
} else {
$output = $output . $currencySeparator . $currencySign;
}
}
return $output;
}
}
@@ -0,0 +1,151 @@
<?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\Fluid\ViewHelpers\Format;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Localization\DateFormatter;
use TYPO3\CMS\Core\Localization\Locale;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to format an object implementing `\DateTimeInterface` into human-readable output.
*
* ```
* <f:format.date format="Y-m-d H:i">{dateObject}</f:format.date>
* <f:format.date format="Y" base="{dateObject}">-1 year</f:format.date>
* <f:format.date pattern="dd. MMMM yyyy" locale="de-DE">{dateObject}</f:format.date>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-format-date
* @see https://www.php.net/manual/datetime.format.php
* @see \DateTimeInterface
*/
final class DateViewHelper extends AbstractViewHelper
{
/**
* Needed as child node's output can return a DateTime object which can't be escaped
*
* @var bool
*/
protected $escapeChildren = false;
public function __construct(
private readonly Context $context
) {}
public function initializeArguments(): void
{
$this->registerArgument('date', 'mixed', 'Either an object implementing DateTimeInterface or a string that is accepted by DateTime constructor');
$this->registerArgument('format', 'string', 'Format String which is taken to format the Date/Time', false, '');
$this->registerArgument('pattern', 'string', 'Format date based on unicode ICO format pattern given see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax. If both "pattern" and "format" arguments are given, pattern will be used.');
$this->registerArgument('locale', 'string', 'A locale format such as "nl-NL" to format the date in a specific locale, if none given, uses the current locale of the current request. Only works when pattern argument is given');
$this->registerArgument('base', 'mixed', 'A base time (an object implementing DateTimeInterface or a string) used if $date is a relative date specification. Defaults to current time.');
$this->registerArgument('timezone', 'string', 'Timezone for the date');
}
public function render(): string
{
$format = $this->arguments['format'] ?? '';
$pattern = $this->arguments['pattern'] ?? null;
$base = $this->arguments['base'] ?? $this->context->getPropertyFromAspect('date', 'timestamp');
if (is_string($base)) {
$base = trim($base);
}
if ($format === '') {
$format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?: 'Y-m-d';
}
$date = $this->renderChildren();
if ($date === null) {
return '';
}
if (is_string($date)) {
$date = trim($date);
}
if ($date === '') {
$date = $this->context->getPropertyFromAspect('date', 'timestamp', 'now');
}
if (!$date instanceof \DateTimeInterface) {
$base = $base instanceof \DateTimeInterface
? (int)$base->format('U')
: (int)strtotime((MathUtility::canBeInterpretedAsInteger($base) ? '@' : '') . $base);
$dateTimestamp = strtotime((MathUtility::canBeInterpretedAsInteger($date) ? '@' : '') . $date, $base);
if ($dateTimestamp === false) {
throw new InvalidArgumentValueException('"' . $date . '" could not be converted to a timestamp. Probably due to a parsing error.', 1241722579);
}
$date = (new \DateTime())->setTimestamp($dateTimestamp);
}
if (!empty($this->arguments['timezone'])) {
$timezone = (string)$this->arguments['timezone'];
if ($date instanceof \DateTime) {
$date->setTimezone(new \DateTimeZone($timezone));
} elseif ($date instanceof \DateTimeImmutable) {
$date = $date->setTimezone(new \DateTimeZone($timezone));
}
}
if ($pattern !== null) {
$locale = $this->arguments['locale'] ?? self::resolveLocale($this->renderingContext);
return (new DateFormatter())->format($date, $pattern, $locale);
}
if (str_contains($format, '%')) {
// @todo: deprecate this syntax in TYPO3 v13.
$locale = $this->arguments['locale'] ?? self::resolveLocale($this->renderingContext);
return (new DateFormatter())->strftime($format, $date, $locale);
}
return $date->format($format);
}
/**
* Explicitly set argument name to be used as content.
*/
public function getContentArgumentName(): string
{
return 'date';
}
private static function resolveLocale(RenderingContextInterface $renderingContext): Locale
{
$request = null;
if ($renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $renderingContext->getAttribute(ServerRequestInterface::class);
} elseif (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) {
// @todo: deprecate
$request = $GLOBALS['TYPO3_REQUEST'];
}
if ($request && ApplicationType::fromRequest($request)->isFrontend()) {
// Frontend application
$siteLanguage = $request->getAttribute('language');
// Get values from site language
if ($siteLanguage !== null) {
return $siteLanguage->getLocale();
}
} elseif (($GLOBALS['BE_USER'] ?? null) instanceof BackendUserAuthentication
&& !empty($GLOBALS['BE_USER']->user['lang'])) {
return new Locale($GLOBALS['BE_USER']->user['lang']);
}
return new Locale();
}
}
@@ -0,0 +1,109 @@
<?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\Fluid\ViewHelpers\Format;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to render a string which can contain HTML markup
* by passing it to a TYPO3 `parseFunc`. This can sanitize
* unwanted HTML tags and attributes, and keep wanted HTML syntax and
* take care of link substitution and other parsing.
* Either specify a path to the TypoScript setting or set the `parseFunc` options directly.
* By default, `lib.parseFunc_RTE` is used to parse the string.
*
* ```
* <f:format.html parseFuncTSPath="lib.myCustomParseFunc">
* {$project} is a cool <b>CMS</b> (<a href="https://www.typo3.org">TYPO3</a>).
* </f:format.html>
* ```
*
* **Note:** The ViewHelper must not be used in backend context, as it triggers frontend logic.
* Instead, use `<f:sanitize.html>` within backend context to secure a given HTML string
* or `<f:transform.html>` to parse links in HTML.
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-format-html
* @see https://docs.typo3.org/permalink/t3tsref:parsefunc
*/
final class HtmlViewHelper extends AbstractViewHelper
{
/**
* Children must not be escaped, to be able to pass {bodytext} directly to it
*
* @var bool
*/
protected $escapeChildren = false;
/**
* Plain HTML should be returned, no output escaping allowed
*
* @var bool
*/
protected $escapeOutput = false;
public function initializeArguments(): void
{
$this->registerArgument('parseFuncTSPath', 'string', 'Path to the TypoScript parseFunc setup.', false, 'lib.parseFunc_RTE');
$this->registerArgument('data', 'mixed', 'Initialize the content object with this set of data. Either an array or object.');
$this->registerArgument('current', 'string', 'Initialize the content object with this value for current property.');
$this->registerArgument('currentValueKey', 'string', 'Define the value key, used to locate the current value for the content object');
$this->registerArgument('table', 'string', 'The table name associated with the "data" argument.', false, '');
}
public function render(): string
{
$parseFuncTSPath = $this->arguments['parseFuncTSPath'];
$data = $this->arguments['data'];
$current = $this->arguments['current'];
$currentValueKey = $this->arguments['currentValueKey'];
$table = $this->arguments['table'];
$request = null;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
$isBackendRequest = $request instanceof ServerRequestInterface && ApplicationType::fromRequest($request)->isBackend();
if ($isBackendRequest) {
throw new \RuntimeException(
'Using f:format.html in backend context is not allowed. Use f:sanitize.html or f:transform.html instead.',
1686813703
);
}
$value = $this->renderChildren() ?? '';
// Prepare data array
if (is_object($data)) {
$data = ObjectAccess::getGettableProperties($data);
} elseif (!is_array($data)) {
$data = (array)$data;
}
$contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObject->setRequest($request);
$contentObject->start($data, $table);
if ($current !== null) {
$contentObject->setCurrentVal($current);
} elseif ($currentValueKey !== null && isset($data[$currentValueKey])) {
$contentObject->setCurrentVal($data[$currentValueKey]);
}
$content = $contentObject->parseFunc($value, null, '< ' . $parseFuncTSPath);
return $content;
}
}
@@ -0,0 +1,83 @@
<?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\Fluid\ViewHelpers\Format;
/**
* ViewHelper to apply `html_entity_decode()` to a value,
* transforming HTML entity representations back into HTML special characters
* (like `&quot;` to `"`).
*
* ```
* <f:format.htmlentitiesDecode>{textWithEntities}</f:format.htmlentitiesDecode>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-format-htmlentitiesdecode
* @see https://www.php.net/html_entity_decode
*/
final class HtmlentitiesDecodeViewHelper extends AbstractEncodingViewHelper
{
/**
* We accept value and children interchangeably, thus we must disable children escaping.
*
* @var bool
*/
protected $escapeChildren = false;
/**
* If we decode, we must not encode again after that.
*
* @var bool
*/
protected $escapeOutput = false;
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('value', 'string', 'string to format');
$this->registerArgument('keepQuotes', 'bool', 'If TRUE, single and double quotes won\'t be replaced (sets ENT_NOQUOTES flag).', false, false);
$this->registerArgument('encoding', 'string', 'Define the encoding used when converting characters (Default: UTF-8).');
}
/**
* Converts all HTML entities to their applicable characters as needed using PHPs html_entity_decode() function.
*
* @see https://www.php.net/html_entity_decode
*/
public function render(): mixed
{
$value = $this->renderChildren();
$encoding = $this->arguments['encoding'];
$keepQuotes = $this->arguments['keepQuotes'];
if (!is_string($value) && !(is_object($value) && method_exists($value, '__toString'))) {
return $value;
}
if ($encoding === null) {
$encoding = self::resolveDefaultEncoding();
}
$flags = $keepQuotes ? ENT_NOQUOTES : ENT_COMPAT;
return html_entity_decode((string)$value, $flags, $encoding);
}
/**
* Explicitly set argument name to be used as content.
*/
public function getContentArgumentName(): string
{
return 'value';
}
}
@@ -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!
*/
namespace TYPO3\CMS\Fluid\ViewHelpers\Format;
/**
* ViewHelper to apply `htmlentities()` escaping to a value,
* transforming all HTML special characters to entity representations
* (like `"` to `&quot;`).
*
* ```
* <f:format.htmlentities>{textWithHtml}</f:format.htmlentities>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-format-htmlentities
* @see https://www.php.net/manual/function.htmlentities.php
*/
final class HtmlentitiesViewHelper extends AbstractEncodingViewHelper
{
/**
* Output gets encoded by this viewhelper
*
* @var bool
*/
protected $escapeOutput = false;
/**
* This prevents double encoding as the whole output gets encoded at the end
*
* @var bool
*/
protected $escapeChildren = false;
public function initializeArguments(): void
{
$this->registerArgument('value', 'string', 'string to format');
$this->registerArgument('keepQuotes', 'bool', 'If TRUE, single and double quotes won\'t be replaced (sets ENT_NOQUOTES flag).', false, false);
$this->registerArgument('encoding', 'string', 'Define the encoding used when converting characters (Default: UTF-8');
$this->registerArgument('doubleEncode', 'bool', 'If FALSE existing html entities won\'t be encoded, the default is to convert everything.', false, true);
}
/**
* Escapes special characters with their escaped counterparts as needed using PHPs htmlentities() function.
*
* @see https://www.php.net/manual/function.htmlentities.php
*/
public function render(): mixed
{
$value = $this->renderChildren();
$encoding = $this->arguments['encoding'];
$keepQuotes = $this->arguments['keepQuotes'];
$doubleEncode = $this->arguments['doubleEncode'];
if (!is_string($value) && !(is_object($value) && method_exists($value, '__toString'))) {
return $value;
}
if ($encoding === null) {
$encoding = self::resolveDefaultEncoding();
}
$flags = $keepQuotes ? ENT_NOQUOTES : ENT_QUOTES;
return htmlentities((string)$value, $flags, $encoding, $doubleEncode);
}
/**
* Explicitly set argument name to be used as content.
*/
public function getContentArgumentName(): string
{
return 'value';
}
}
@@ -0,0 +1,80 @@
<?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\Fluid\ViewHelpers\Format;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to format a string to specific lengths, by using PHPs `str_pad` function.
*
* ```
* <f:format.padding padLength="10" padString="!" padType="right">TYPO3</f:format.padding>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-format-padding
* @see https://www.php.net/manual/en/function.str-pad
*/
final class PaddingViewHelper extends AbstractViewHelper
{
/**
* Output is escaped already. We must not escape children, to avoid double encoding.
*
* @var bool
*/
protected $escapeChildren = false;
public function initializeArguments(): void
{
$this->registerArgument('value', 'string', 'string to format');
$this->registerArgument('padLength', 'int', 'Length of the resulting string. If the value of pad_length is negative or less than the length of the input string, no padding takes place.', true);
$this->registerArgument('padString', 'string', 'The padding string', false, ' ');
$this->registerArgument('padType', 'string', 'Append the padding at this site (Possible values: right,left,both. Default: right)', false, 'right');
}
/**
* Pad a string to a certain length with another string.
*/
public function render(): string
{
$value = $this->renderChildren();
$padTypes = [
'left' => STR_PAD_LEFT,
'right' => STR_PAD_RIGHT,
'both' => STR_PAD_BOTH,
];
$padType = $this->arguments['padType'];
if (!isset($padTypes[$padType])) {
$padType = 'right';
}
$value = (string)$value;
$padString = (string)$this->arguments['padString'];
// mb_str_pad() throws a ValueError on an empty pad string, so return the value unchanged in that case.
if ($padString === '') {
return $value;
}
return mb_str_pad($value, (int)$this->arguments['padLength'], $padString, $padTypes[$padType]);
}
/**
* Explicitly set argument name to be used as content.
*/
public function getContentArgumentName(): string
{
return 'value';
}
}
@@ -0,0 +1,158 @@
<?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\Fluid\ViewHelpers\Image;
use TYPO3\CMS\Core\Html\Srcset\SrcsetAttribute;
use TYPO3\CMS\Core\Html\Srcset\WidthSrcsetCandidate;
use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to generate a list of image URLs and their corresponding srcset
* descriptors to be used in a srcset attribute of an <img> or <source> tag.
*
* Responsive images can either be defined based on absolute widths or relative pixel densities.
*
* Width descriptors use the "w" unit, which refers to the width of the image file (not to be
* confused with "px", which refers to so-called CSS pixels in the HTML document). The srcset
* attribute in combination with the sizes attribute provide hints to the browser which one
* of the provided image files should be used. The browser is free to consider other factors,
* such as network speed, user preferences, or client-side caching status.
*
* Density descriptors use the "x" unit and target client devices based on the pixel density
* of their screens. "1x" will be loaded for low-density devices, while "2x", "3x"... target
* higher densities, also referred to as "High DPI" or "Retina".
*
* According to the HTML standard, "w" and "x" units cannot be mixed. Instead, browsers will
* usually consider the pixel density, even if "w" units are used. In practice, "x" is only
* relevant for fixed-width images across device sizes (e. g. an icon that always has the same
* visual size). In all other cases, "w" should be preferred.
*
* Examples
* ========
*
* Width descriptors
* -----------------
*
* ```
* <source srcset="{f:image.srcset(image: imageObject, srcset: '1000w, 1200w, 1400w', cropVariant: 'desktop')}" media="(min-width: 1000px)" sizes="100vw" />
* ```
*
* Output::
*
* ```
* <source srcset="/path/to/csm_myimage_1000.jpg 1000w, /path/to/csm_myimage_1200.jpg 1200w, /path/to/csm_myimage_1400.jpg 1400w" media="(min-width: 1000px)" sizes="100vw" />
* ```
*
* Density descriptors
* -------------------
*
* <source srcset="{f:image.srcset(image: imageObject, srcset: '1x, 2x', referenceWidth: 500, cropVariant: 'desktop')}" media="(min-width: 1000px)" />
*
* Output::
*
* <source srcset="/path/to/csm_myimage_500.jpg 1x, /path/to/csm_myimage_1000.jpg 2x" media="(min-width: 1000px)" />
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-image-srcset
*/
final class SrcsetViewHelper extends AbstractViewHelper
{
public function __construct(private readonly ImageService $imageService) {}
public function initializeArguments(): void
{
$this->registerArgument('image', FileInterface::class, 'A FAL object (\\TYPO3\\CMS\\Core\\Resource\\File or \\TYPO3\\CMS\\Core\\Resource\\FileReference); if not specified, ViewHelper children will be used as a fallback.');
$this->registerArgument('srcset', 'string', 'Comma-separated list of width descriptors (e. g. 200w) or pixel density descriptors (e. g. 2x).', true);
$this->registerArgument('referenceWidth', 'int', 'Image width that will be used as base (1x) when calculating srcset with pixel density descriptors (e. g. 2x). This is irrelevant for width descriptors.');
$this->registerArgument('crop', 'string|bool|array', 'Overrule cropping of image (setting to FALSE disables the cropping set in FileReference)');
$this->registerArgument('cropVariant', 'string', 'Select a cropping variant, in case multiple croppings have been specified or stored in FileReference', false, 'default');
$this->registerArgument('fileExtension', 'string', 'Use the specified target file extension for generated images; files will be converted if necessary');
$this->registerArgument('absolute', 'bool', 'Force absolute URL for generated images', false, false);
}
public function render(): SrcsetAttribute
{
$image = $this->arguments['image'] ?? $this->renderChildren();
if (!$image instanceof FileInterface) {
throw new InvalidArgumentValueException('A valid file object must be specified.', 1697797783);
}
$fileExtension = $this->validateFileExtension($this->arguments['fileExtension']);
$cropArea = $this->getCropAreaFromArguments($image, $this->arguments['crop'], $this->arguments['cropVariant']);
$cropArea = $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($image);
$srcsetAsArray = GeneralUtility::trimExplode(',', $this->arguments['srcset'], true);
try {
$srcset = SrcsetAttribute::createFromDescriptors($srcsetAsArray, $this->arguments['referenceWidth']);
} catch (\Exception $e) {
throw new InvalidArgumentValueException('Invalid srcset configuration provided: ' . $e->getMessage(), 1774530722, $e);
}
foreach ($srcset->getCandidates() as $candidate) {
$processedImage = $this->imageService->applyProcessingInstructions($image, [
'width' => $candidate->getCalculatedWidth(),
'crop' => $cropArea,
...($fileExtension ? ['fileExtension' => $fileExtension] : []),
]);
// If processor_allowUpscaling is set to false and a bigger image than the original was requested,
// the srcset string should still offer the maximum image size available as a fallback, even if this
// diverts from the specific configuration. In this case, width descriptors need to be updated to
// match the actual width of the generated image file. This might lead to duplicate files in the first
// place, but descriptors are used as array keys, so they won't appear as duplicates in the markup
if ($candidate instanceof WidthSrcsetCandidate && $processedImage->getProperty('width') !== $candidate->getCalculatedWidth()) {
$candidate->setWidth($processedImage->getProperty('width'));
}
$candidate->setUri($this->imageService->getImageUri($processedImage, $this->arguments['absolute']));
}
return $srcset;
}
protected function getCropAreaFromArguments(FileInterface $image, $crop, string $cropVariant): Area
{
if ($crop === null && $image->hasProperty('crop') && $image->getProperty('crop')) {
$crop = $image->getProperty('crop');
}
$cropVariantsCollection = CropVariantCollection::create(is_array($crop) ? json_encode($crop) : (string)$crop);
return $cropVariantsCollection->getCropArea($cropVariant);
}
protected function validateFileExtension(?string $fileExtension): ?string
{
if ($fileExtension === null) {
return null;
}
if (!GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileExtension)) {
throw new InvalidArgumentValueException(
'The extension ' . $fileExtension . ' is not specified in $GLOBALS[\'TYPO3_CONF_VARS\'][\'GFX\'][\'imagefile_ext\']'
. ' as a valid image file extension and can not be processed.',
1697797923
);
}
return $fileExtension;
}
}
+216
View File
@@ -0,0 +1,216 @@
<?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\Fluid\ViewHelpers;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to resize, crop or convert a given image (if required) and render
* the corresponding HTML `<img>` tag showing the processed image.
*
* Note that image operations (cropping, scaling, converting) on
* non-FAL files (i.e. extension resources) may be changed in future TYPO3
* versions, since those operations are coupled with FAL metadata. Each
* non-FAL image operation creates a "fake" FAL record, which may lead to problems.
*
* External URLs are not processed.
*
* ```
* <f:image src="EXT:myext/Resources/Public/typo3_logo.png" width="100c" />
* <f:image fileExtension="webp" image="{imageObject}" maxWidth="400" maxHeight="400" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-image
*/
final class ImageViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'img';
public function __construct(
private readonly ImageService $imageService
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('src', 'string', 'a path to a file, a combined FAL identifier or an uid (int). If $treatIdAsReference is set, the integer is considered the uid of the sys_file_reference record. If you already got a FAL object, consider using the $image parameter instead', false, '');
$this->registerArgument('treatIdAsReference', 'bool', 'given src argument is a sys_file_reference record', false, false);
$this->registerArgument('image', 'object', 'a FAL object (\\TYPO3\\CMS\\Core\\Resource\\File or \\TYPO3\\CMS\\Core\\Resource\\FileReference)');
$this->registerArgument('crop', 'string|bool|array', 'overrule cropping of image (setting to FALSE disables the cropping set in FileReference)');
$this->registerArgument('cropVariant', 'string', 'select a cropping variant, in case multiple croppings have been specified or stored in FileReference', false, 'default');
$this->registerArgument('fileExtension', 'string', 'Custom file extension to use');
$this->registerArgument('width', 'string', 'width of the image. This can be a numeric value representing the fixed width of the image in pixels. But you can also perform simple calculations by adding "m" or "c" to the value. See imgResource.width in the TypoScript Reference on https://docs.typo3.org/permalink/t3tsref:confval-imgresource-width for possible options.');
$this->registerArgument('height', 'string', 'height of the image. This can be a numeric value representing the fixed height of the image in pixels. But you can also perform simple calculations by adding "m" or "c" to the value. See imgResource.height in the TypoScript Reference https://docs.typo3.org/permalink/t3tsref:confval-imgresource-height for possible options.');
$this->registerArgument('minWidth', 'int', 'minimum width of the image');
$this->registerArgument('minHeight', 'int', 'minimum height of the image');
$this->registerArgument('maxWidth', 'int', 'maximum width of the image');
$this->registerArgument('maxHeight', 'int', 'maximum height of the image');
$this->registerArgument('absolute', 'bool', 'Force absolute URL', false, false);
$this->registerArgument('base64', 'bool', 'Adds the image data base64-encoded inline to the images "src" attribute. Useful for FluidEmail templates.', false, false);
}
/**
* Resizes a given image (if required) and renders the respective img tag.
*
* @see https://docs.typo3.org/typo3cms/TyposcriptReference/ContentObjects/Image/
*/
public function render(): string
{
$src = (string)$this->arguments['src'];
if (($src === '' && $this->arguments['image'] === null) || ($src !== '' && $this->arguments['image'] !== null)) {
throw new InvalidArgumentValueException($this->getExceptionMessage('You must either specify a string src or a File object.'), 1382284106);
}
if ((string)$this->arguments['fileExtension'] && !GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], (string)$this->arguments['fileExtension'])) {
throw new InvalidArgumentValueException(
$this->getExceptionMessage(
'The extension ' . $this->arguments['fileExtension'] . ' is not specified in $GLOBALS[\'TYPO3_CONF_VARS\'][\'GFX\'][\'imagefile_ext\']'
. ' as a valid image file extension and can not be processed.',
),
1618989190
);
}
try {
$image = $this->imageService->getImage($src, $this->arguments['image'], (bool)$this->arguments['treatIdAsReference']);
if ($this->isUnavailable($image)) {
return '';
}
$cropString = $this->arguments['crop'];
if ($cropString === null && $image->hasProperty('crop') && $image->getProperty('crop')) {
$cropString = $image->getProperty('crop');
}
// CropVariantCollection needs a string, but this VH could also receive an array
if (is_array($cropString)) {
$cropString = json_encode($cropString);
}
$cropVariantCollection = CropVariantCollection::create((string)$cropString);
$cropVariant = $this->arguments['cropVariant'] ?: 'default';
$cropArea = $cropVariantCollection->getCropArea($cropVariant);
$processingInstructions = [
'width' => $this->arguments['width'],
'height' => $this->arguments['height'],
'minWidth' => $this->arguments['minWidth'],
'minHeight' => $this->arguments['minHeight'],
'maxWidth' => $this->arguments['maxWidth'],
'maxHeight' => $this->arguments['maxHeight'],
'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($image),
];
if (!empty($this->arguments['fileExtension'] ?? '')) {
$processingInstructions['fileExtension'] = $this->arguments['fileExtension'];
}
$processedImage = $this->imageService->applyProcessingInstructions($image, $processingInstructions);
if ($this->arguments['base64']) {
$imageSrc = 'data:' . $processedImage->getMimeType() . ';base64,' . base64_encode($processedImage->getContents());
} else {
$imageSrc = $this->imageService->getImageUri($processedImage, $this->arguments['absolute']);
if ($imageSrc === '') {
// No public URL could be determined, for instance because the file resides in a
// non-public storage and no request is available to create a file dump URL from.
return '';
}
}
if (!$this->tag->hasAttribute('data-focus-area')) {
$focusArea = $cropVariantCollection->getFocusArea($cropVariant);
if (!$focusArea->isEmpty()) {
$this->tag->addAttribute('data-focus-area', (string)$focusArea->makeAbsoluteBasedOnFile($image));
}
}
$this->tag->addAttribute('src', $imageSrc);
$this->tag->addAttribute('width', $processedImage->getProperty('width'));
$this->tag->addAttribute('height', $processedImage->getProperty('height'));
if (isset($this->additionalArguments['alt']) && $this->additionalArguments['alt'] === '') {
// In case the "alt" attribute is explicitly set to an empty string, respect
// this to allow excluding it from screen readers, improving accessibility.
$this->tag->addAttribute('alt', '');
} elseif (!isset($this->additionalArguments['alt'])) {
// The alt-attribute is mandatory to have valid html-code, therefore use "alternative" property or empty
$this->tag->addAttribute('alt', $image->getProperty('alternative') ?? '');
}
// Only add title-attribute from image if not set in additional-arguments.
// In case the "title" attribute is explicitly set to an empty string,
// it will not fallback to an image-title.
// This allows excluding it explicitly from screen readers, improving accessibility.
if (!isset($this->additionalArguments['title'])) {
$title = trim((string)($image->hasProperty('title') ? $image->getProperty('title') : ''));
// The title-attribute is not mandatory, therefore use "title" property or omit fully
if ($title !== '') {
$this->tag->addAttribute('title', $title);
}
}
} catch (ResourceDoesNotExistException $e) {
// thrown if file does not exist
throw new Exception($this->getExceptionMessage($e->getMessage()), 1509741911, $e);
} catch (\UnexpectedValueException $e) {
// thrown if a file has been replaced with a folder
throw new Exception($this->getExceptionMessage($e->getMessage()), 1509741912, $e);
} catch (\InvalidArgumentException $e) {
// thrown if file storage does not exist
throw new Exception($this->getExceptionMessage($e->getMessage()), 1509741914, $e);
}
return $this->tag->render();
}
/**
* A file that has been flagged as missing by the file indexer, that has been deleted, or that
* resides in an offline storage can not be processed and has no public URL. Rendering an "img"
* tag for it would result in an empty "src" attribute, so nothing is rendered instead.
*/
private function isUnavailable(FileInterface $image): bool
{
$file = $image instanceof FileReference ? $image->getOriginalFile() : $image;
if (!$file instanceof File) {
return false;
}
return $file->isMissing() || $file->isDeleted() || !$file->getStorage()->isOnline();
}
private function getExceptionMessage(string $detailedMessage): string
{
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
$currentContentObject = $request->getAttribute('currentContentObject');
if ($currentContentObject instanceof ContentObjectRenderer) {
return sprintf('Unable to render image tag in "%s": %s', $currentContentObject->currentRecord, $detailedMessage);
}
}
return "Unable to render image tag: $detailedMessage";
}
}
@@ -0,0 +1,101 @@
<?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\Fluid\ViewHelpers\Link;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Extbase\Mvc\RequestInterface as ExtbaseRequestInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper for creating links to Extbase actions. Tailored for Extbase
* plugins, uses Extbase Request and Extbase UriBuilder.
*
* ```
* <f:link.action action="show" arguments="{blog: blog.uid}">action link</f:link.action>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-link-action
*/
final class ActionViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'a';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('action', 'string', 'Target action');
$this->registerArgument('arguments', 'array', 'Arguments for the controller action, associative array (do not use reserved keywords "action", "controller" or "format" if not referring to these internal variables specifically)', false, []);
$this->registerArgument('controller', 'string', 'Target controller. If NULL current controllerName is used');
$this->registerArgument('extensionName', 'string', 'Target Extension Name (without `tx_` prefix and no underscores). If NULL the current extension name is used');
$this->registerArgument('pluginName', 'string', 'Target plugin. If empty, the current plugin name is used');
$this->registerArgument('pageUid', 'int', 'Target page. See TypoLink destination');
$this->registerArgument('pageType', 'int', 'Type of the target page. See typolink.parameter', false, 0);
$this->registerArgument('noCache', 'bool', 'Set this to disable caching for the target page. You should not need this.');
$this->registerArgument('language', 'string', 'link to a specific language - defaults to the current language, use a language ID or "current" to enforce a specific language');
$this->registerArgument('section', 'string', 'The anchor to be added to the URI', false, '');
$this->registerArgument('format', 'string', 'The requested format, e.g. ".html', false, '');
$this->registerArgument('linkAccessRestrictedPages', 'bool', 'If set, links pointing to access restricted pages will still link to the page even though the page cannot be accessed.', false, false);
$this->registerArgument('additionalParams', 'array', 'Additional query parameters that won\'t be prefixed like $arguments (overrule $arguments)', false, []);
$this->registerArgument('absolute', 'bool', 'If set, the URI of the rendered link is absolute', false, false);
$this->registerArgument('addQueryString', 'string', 'If set, the current query parameters will be kept in the URL. If set to "untrusted", then ALL query parameters will be added. Be aware, that this might lead to problems when the generated link is cached.', false, false);
$this->registerArgument('argumentsToBeExcludedFromQueryString', 'array', 'Arguments to be removed from the URI. Only active if $addQueryString = true', false, []);
}
public function render(): string
{
$request = null;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
// Since f:uri.action and f:link.action use exactly the same ViewHelper arguments,
// the glue code between the ViewHelper API and TYPO3's URI generation is shared across both ViewHelpers.
$childContent = (string)$this->renderChildren();
if ($request instanceof ExtbaseRequestInterface) {
$uri = \TYPO3\CMS\Fluid\ViewHelpers\Uri\ActionViewHelper::createUriWithExtbaseContext($request, $this->arguments);
if ($uri === '') {
return $childContent;
}
$this->tag->addAttribute('href', $uri);
$this->tag->setContent($childContent);
$this->tag->forceClosingTag(true);
return $this->tag->render();
}
if ($request instanceof ServerRequestInterface && ApplicationType::fromRequest($request)->isFrontend()) {
$linkResult = \TYPO3\CMS\Fluid\ViewHelpers\Uri\ActionViewHelper::createFrontendLinkWithCoreContext($request, $this->arguments, $childContent);
if ($linkResult === null) {
return $childContent;
}
// Removing TypoLink target here to ensure same behaviour with extbase uri builder in this context.
$linkResultAttributes = $linkResult->getAttributes();
unset($linkResultAttributes['target']);
$this->tag->addAttributes($linkResultAttributes);
$this->tag->setContent($childContent);
$this->tag->forceClosingTag(true);
return $this->tag->render();
}
throw new \RuntimeException(
'The rendering context of ViewHelper f:link.action is missing a valid request object.',
1690365240
);
}
}
@@ -0,0 +1,88 @@
<?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\Fluid\ViewHelpers\Link;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\LinkHandling\EmailLinkHandler;
use TYPO3\CMS\Frontend\Typolink\LinkFactory;
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper to generate an email link (`mailto:`), respecting TYPO3s `spamProtectEmailAddresses` TypoScript setting.
*
* ```
* <f:link.email email="foo@example.com" subject="Website contact" cc="fooSupervisor@example.com" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-link-email
* @see https://docs.typo3.org/permalink/t3tsref:confval-config-spamprotectemailaddresses
*/
final class EmailViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'a';
public function __construct(
private readonly EmailLinkHandler $emailLinkHandler,
private readonly LinkFactory $linkFactory,
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('email', 'string', 'The email address to be turned into a link', true);
$this->registerArgument('cc', 'string', 'The email address(es) for CC of the email link');
$this->registerArgument('bcc', 'string', 'The email address(es) for BCC of the email link');
$this->registerArgument('subject', 'string', 'A prefilled subject for the email link');
$this->registerArgument('body', 'string', 'A prefilled body for the email link');
}
public function render(): string
{
$email = $this->arguments['email'];
$linkHref = $this->emailLinkHandler->asString($this->arguments);
$attributes = [];
$linkText = htmlspecialchars($email);
$request = $this->renderingContext->hasAttribute(ServerRequestInterface::class) ? $this->renderingContext->getAttribute(ServerRequestInterface::class) : null;
if ($request !== null && ApplicationType::fromRequest($request)->isFrontend()) {
// If there is no request, backend is assumed.
try {
$linkResult = $this->linkFactory->create($linkText, ['parameter' => $linkHref], $request->getAttribute('currentContentObject'));
$linkText = (string)$linkResult->getLinkText();
$attributes = $linkResult->getAttributes();
} catch (UnableToLinkException) {
// Just render the email as is (= Backend Context), if LinkBuilder failed
}
}
$tagContent = $this->renderChildren();
if ($tagContent !== null) {
$linkText = (string)$tagContent;
}
$this->tag->setContent($linkText);
$this->tag->addAttribute('href', $linkHref);
$this->tag->forceClosingTag(true);
$this->tag->addAttributes($attributes);
return $this->tag->render();
}
}
@@ -0,0 +1,60 @@
<?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\Fluid\ViewHelpers\Link;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper for creating links to external targets.
*
* ```
* <f:link.external uri="https://www.typo3.org" target="_blank">external link</f:link.external>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-link-external
*/
final class ExternalViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'a';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('uri', 'string', 'The URI that will be put in the href attribute of the rendered link tag', true);
$this->registerArgument('defaultScheme', 'string', 'Scheme the href attribute will be prefixed with if specified $uri does not contain a scheme already', false, 'https');
}
public function render(): string
{
$uri = $this->arguments['uri'];
$defaultScheme = $this->arguments['defaultScheme'];
$scheme = parse_url($uri, PHP_URL_SCHEME);
if ($scheme === null && $defaultScheme !== '') {
$uri = $defaultScheme . '://' . $uri;
}
$this->tag->addAttribute('href', $uri);
$this->tag->setContent((string)$this->renderChildren());
$this->tag->forceClosingTag(true);
return $this->tag->render();
}
}
+157
View File
@@ -0,0 +1,157 @@
<?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\Fluid\ViewHelpers\Link;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper for creating links to a file (FAL).
*
* ```
* <f:link.file file="{file}" target="_blank" download="true" filename="some-file.pdf">See file</f:link.file>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-link-file
*/
final class FileViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'a';
public function __construct(
private readonly HashService $hashService
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('file', FileInterface::class, 'Specifies the file to create a link to', true);
$this->registerArgument('download', 'bool', 'Specifies if file should be downloaded instead of displayed');
$this->registerArgument('filename', 'string', 'Specifies an alternative filename. If filename contains a file extension, this must be the same as from \'file\'.');
}
public function render(): string
{
$file = $this->arguments['file'];
if (!($file instanceof FileInterface)) {
throw new InvalidArgumentValueException('Argument \'file\' must be an instance of ' . FileInterface::class, 1621511632);
}
// Get the public URL. This url is either be defined by a GeneratePublicUrlForResourceEvent,
// an OnlineMedia helper, the corresponding driver or using the file dump functionality.
$publicUrl = $file->getPublicUrl();
// Early return in case public url is null as this indicates the file is
// not accessible, e.g. because the corresponding storage is offline.
if ($publicUrl === null) {
return '';
}
if (str_contains($publicUrl, 'dumpFile')) {
// In case we deal with is a file dump URL, recreate the URL
// by taking the defined view helper arguments into account.
$publicUrl = $this->createFileDumpUrl($file);
} elseif ($this->arguments['download'] ?? false) {
// In case the URL directly links to the file (no eID) and
// the file should be downloaded instead of displayed, this
// must be set by the "download" tag attribute, which may
// contain an alternative filename.
$this->tag->addAttribute(
'download',
$this->getAlternativeFilename($file)
);
}
$this->tag->addAttribute('href', $publicUrl);
$childContent = $this->renderChildren();
$this->tag->setContent($childContent ? (string)$childContent : htmlspecialchars($file->getName()));
$this->tag->forceClosingTag(true);
return $this->tag->render();
}
/**
* Create a file dump URL, taking the view helper arguments into account
*/
private function createFileDumpUrl(FileInterface $file): string
{
$parameters = ['eID' => 'dumpFile'];
if ($file instanceof File) {
$parameters['t'] = 'f';
$parameters['f'] = $file->getUid();
} elseif ($file instanceof FileReference) {
$parameters['t'] = 'r';
$parameters['r'] = $file->getUid();
} elseif ($file instanceof ProcessedFile) {
$parameters['t'] = 'p';
$parameters['p'] = $file->getUid();
}
if ($download = $this->arguments['download'] ?? false) {
$parameters['dl'] = (int)$download;
}
if (($filename = $this->getAlternativeFilename($file)) !== '') {
$parameters['fn'] = $filename;
}
$parameters['token'] = $this->hashService->hmac(implode('|', $parameters), 'resourceStorageDumpFile');
return GeneralUtility::locationHeaderUrl(
PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'),
$this->renderingContext->getAttribute(ServerRequestInterface::class)
) . '?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
}
private function getAlternativeFilename(FileInterface $file): string
{
$alternativeFilename = $this->arguments['filename'] ?? '';
// Return early if filename is empty or not valid
if ($alternativeFilename === '' || !preg_match('/^[0-9a-z._\-]+$/i', $alternativeFilename)) {
return '';
}
$extension = pathinfo($alternativeFilename, PATHINFO_EXTENSION);
if ($extension === '') {
// Add original extension in case alternative filename did not contain any
$alternativeFilename = rtrim($alternativeFilename, '.') . '.' . $file->getExtension();
}
// Check if given or resolved extension matches the original one
return $file->getExtension() === pathinfo($alternativeFilename, PATHINFO_EXTENSION)
? $alternativeFilename
: '';
}
}
+252
View File
@@ -0,0 +1,252 @@
<?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\Fluid\ViewHelpers\Link;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Routing\UriBuilder as BackendUriBuilder;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Extbase\Mvc\RequestInterface as ExtbaseRequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder as ExtbaseUriBuilder;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Typolink\LinkFactory;
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper for creating links to TYPO3 pages.
*
* ```
* <f:link.page pageUid="42" additionalParams="{foo: 'bar'}">page link</f:link.page>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-link-page
*/
final class PageViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'a';
public function __construct(
private readonly BackendUriBuilder $uriBuilder,
private readonly LinkFactory $linkFactory
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('pageUid', 'int', 'Target page. See TypoLink destination');
$this->registerArgument('pageType', 'int', 'Type of the target page. See typolink.parameter');
$this->registerArgument('noCache', 'bool', 'Set this to disable caching for the target page. You should not need this.');
$this->registerArgument('language', 'string', 'link to a specific language - defaults to the current language, use a language ID or "current" to enforce a specific language');
$this->registerArgument('section', 'string', 'The anchor to be added to the URI');
$this->registerArgument('linkAccessRestrictedPages', 'bool', 'If set, links pointing to access restricted pages will still link to the page even though the page cannot be accessed.');
$this->registerArgument('additionalParams', 'array', 'Additional query parameters that won\'t be prefixed like $arguments (overrule $arguments)');
$this->registerArgument('absolute', 'bool', 'If set, the URI of the rendered link is absolute');
$this->registerArgument('addQueryString', 'string', 'If set, the current query parameters will be kept in the URL. If set to "untrusted", then ALL query parameters will be added. Be aware, that this might lead to problems when the generated link is cached.', false, false);
$this->registerArgument('argumentsToBeExcludedFromQueryString', 'array', 'Arguments to be removed from the URI. Only active if $addQueryString = true');
}
public function render(): string
{
$request = null;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
if ($request instanceof ExtbaseRequestInterface) {
return $this->renderWithExtbaseContext($request);
}
if ($request instanceof ServerRequestInterface) {
if (ApplicationType::fromRequest($request)->isFrontend()) {
// Use the regular typolink functionality.
return $this->renderFrontendLinkWithCoreContext($request);
}
$uri = $this->renderBackendLinkWithCoreContext($request);
if ($uri !== '') {
$this->tag->addAttribute('href', $uri);
$this->tag->setContent((string)$this->renderChildren());
$this->tag->forceClosingTag(true);
$result = $this->tag->render();
} else {
$result = (string)$this->renderChildren();
}
return $result;
}
throw new \RuntimeException(
'The rendering context of ViewHelper f:link.page is missing a valid request object.',
1639819269
);
}
private function renderFrontendLinkWithCoreContext(ServerRequestInterface $request): string
{
$pageUid = isset($this->arguments['pageUid']) ? (int)$this->arguments['pageUid'] : 'current';
$pageType = isset($this->arguments['pageType']) ? (int)$this->arguments['pageType'] : 0;
$noCache = isset($this->arguments['noCache']) && (bool)$this->arguments['noCache'];
$section = isset($this->arguments['section']) ? (string)$this->arguments['section'] : '';
$language = isset($this->arguments['language']) ? (string)$this->arguments['language'] : null;
$linkAccessRestrictedPages = isset($this->arguments['linkAccessRestrictedPages']) && (bool)$this->arguments['linkAccessRestrictedPages'];
$additionalParams = isset($this->arguments['additionalParams']) ? (array)$this->arguments['additionalParams'] : [];
$absolute = isset($this->arguments['absolute']) && (bool)$this->arguments['absolute'];
$addQueryString = $this->arguments['addQueryString'] ?? false;
$argumentsToBeExcludedFromQueryString = isset($this->arguments['argumentsToBeExcludedFromQueryString']) ? (array)$this->arguments['argumentsToBeExcludedFromQueryString'] : [];
$typolinkConfiguration = [
'parameter' => $pageUid,
];
if ($pageType) {
$typolinkConfiguration['parameter'] .= ',' . $pageType;
}
if ($noCache) {
$typolinkConfiguration['no_cache'] = 1;
}
if ($language !== null) {
$typolinkConfiguration['language'] = $language;
}
if ($section) {
$typolinkConfiguration['section'] = $section;
}
if ($linkAccessRestrictedPages) {
$typolinkConfiguration['linkAccessRestrictedPages'] = 1;
}
if ($additionalParams) {
$typolinkConfiguration['queryParameters'] = $additionalParams;
}
if ($absolute) {
$typolinkConfiguration['forceAbsoluteUrl'] = true;
}
if ($addQueryString && $addQueryString !== 'false') {
$typolinkConfiguration['addQueryString'] = $addQueryString;
if ($argumentsToBeExcludedFromQueryString !== []) {
$typolinkConfiguration['addQueryString.']['exclude'] = implode(',', $argumentsToBeExcludedFromQueryString);
}
}
try {
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$linkResult = $this->linkFactory->create((string)$this->renderChildren(), $typolinkConfiguration, $cObj);
// Removing TypoLink target here to ensure same behaviour with extbase uri builder in this context.
$linkResultAttributes = $linkResult->getAttributes();
unset($linkResultAttributes['target']);
$this->tag->addAttributes($linkResultAttributes);
$this->tag->setContent((string)$this->renderChildren());
$this->tag->forceClosingTag(true);
$result = $this->tag->render();
} catch (UnableToLinkException) {
$result = (string)$this->renderChildren();
}
return $result;
}
private function renderBackendLinkWithCoreContext(ServerRequestInterface $request): string
{
$pageUid = isset($this->arguments['pageUid']) ? (int)$this->arguments['pageUid'] : null;
$section = isset($this->arguments['section']) ? (string)$this->arguments['section'] : '';
$additionalParams = isset($this->arguments['additionalParams']) ? (array)$this->arguments['additionalParams'] : [];
$absolute = isset($this->arguments['absolute']) && (bool)$this->arguments['absolute'];
$addQueryString = $this->arguments['addQueryString'] ?? false;
$argumentsToBeExcludedFromQueryString = isset($this->arguments['argumentsToBeExcludedFromQueryString']) ? (array)$this->arguments['argumentsToBeExcludedFromQueryString'] : [];
$arguments = [];
if ($addQueryString && $addQueryString !== 'false') {
$arguments = $request->getQueryParams();
foreach ($argumentsToBeExcludedFromQueryString as $argumentToBeExcluded) {
$argumentArrayToBeExcluded = [];
parse_str($argumentToBeExcluded, $argumentArrayToBeExcluded);
$arguments = ArrayUtility::arrayDiffKeyRecursive($arguments, $argumentArrayToBeExcluded);
}
}
$id = $pageUid ?? $request->getQueryParams()['id'] ?? null;
if ($id !== null) {
$arguments['id'] = $id;
}
if (!isset($arguments['route']) && ($route = $request->getAttribute('route')) instanceof Route) {
$arguments['route'] = $route->getOption('_identifier');
}
$arguments = array_replace_recursive($arguments, $additionalParams);
$routeName = $arguments['route'] ?? null;
unset($arguments['route'], $arguments['token']);
try {
if ($absolute) {
$uri = (string)$this->uriBuilder->buildUriFromRoute($routeName, $arguments, BackendUriBuilder::ABSOLUTE_URL);
} else {
$uri = (string)$this->uriBuilder->buildUriFromRoute($routeName, $arguments, BackendUriBuilder::ABSOLUTE_PATH);
}
} catch (RouteNotFoundException) {
$uri = '';
}
if ($section !== '') {
$uri .= '#' . $section;
}
return $uri;
}
private function renderWithExtbaseContext(ExtbaseRequestInterface $request): string
{
$pageUid = isset($this->arguments['pageUid']) ? (int)$this->arguments['pageUid'] : null;
$pageType = isset($this->arguments['pageType']) ? (int)$this->arguments['pageType'] : 0;
$noCache = isset($this->arguments['noCache']) && (bool)$this->arguments['noCache'];
$section = isset($this->arguments['section']) ? (string)$this->arguments['section'] : '';
$language = isset($this->arguments['language']) ? (string)$this->arguments['language'] : null;
$linkAccessRestrictedPages = isset($this->arguments['linkAccessRestrictedPages']) && (bool)$this->arguments['linkAccessRestrictedPages'];
$additionalParams = isset($this->arguments['additionalParams']) ? (array)$this->arguments['additionalParams'] : [];
$absolute = isset($this->arguments['absolute']) && (bool)$this->arguments['absolute'];
$addQueryString = $this->arguments['addQueryString'] ?? false;
$argumentsToBeExcludedFromQueryString = isset($this->arguments['argumentsToBeExcludedFromQueryString']) ? (array)$this->arguments['argumentsToBeExcludedFromQueryString'] : [];
$uriBuilder = GeneralUtility::makeInstance(ExtbaseUriBuilder::class);
$uriBuilder->reset()
->setRequest($request)
->setTargetPageType($pageType)
->setNoCache($noCache)
->setSection($section)
->setLanguage($language)
->setLinkAccessRestrictedPages($linkAccessRestrictedPages)
->setArguments($additionalParams)
->setCreateAbsoluteUri($absolute)
->setAddQueryString($addQueryString)
->setArgumentsToBeExcludedFromQueryString($argumentsToBeExcludedFromQueryString);
if (MathUtility::canBeInterpretedAsInteger($pageUid)) {
$uriBuilder->setTargetPageUid((int)$pageUid);
}
$uri = $uriBuilder->build();
if ($uri !== '') {
$this->tag->addAttribute('href', $uri);
$this->tag->setContent((string)$this->renderChildren());
$this->tag->forceClosingTag(true);
$result = $this->tag->render();
} else {
$result = (string)$this->renderChildren();
}
return $result;
}
}
@@ -0,0 +1,177 @@
<?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\Fluid\ViewHelpers\Link;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
use TYPO3\CMS\Core\LinkHandling\TypolinkParameter;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3Fluid\Fluid\Core\Variables\ScopedVariableProvider;
use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to create links from fields supported by the link wizard
*
* ```
* <f:link.typolink parameter="123" additionalParams="&u=b" language="2" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-link-typolink
*/
final class TypolinkViewHelper extends AbstractViewHelper
{
/**
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly TypoLinkCodecService $typoLinkCodecService
) {}
public function initializeArguments(): void
{
$this->registerArgument('parameter', 'mixed', 'stdWrap.typolink style parameter string', true);
$this->registerArgument('target', 'string', 'Define where to display the linked URL', false, '');
$this->registerArgument('class', 'string', 'Define classes for the link element', false, '');
$this->registerArgument('title', 'string', 'Define the title for the link element', false, '');
$this->registerArgument('language', 'string', 'link to a specific language - defaults to the current language, use a language ID or "current" to enforce a specific language');
$this->registerArgument('additionalParams', 'string', 'Additional query parameters to be attached to the resulting URL', false, '');
$this->registerArgument('additionalAttributes', 'array', 'Additional tag attributes to be added directly to the resulting HTML tag', false, []);
$this->registerArgument('addQueryString', 'string', 'If set, the current query parameters will be kept in the URL. If set to "untrusted", then ALL query parameters will be added. Be aware, that this might lead to problems when the generated link is cached.', false, false);
$this->registerArgument('addQueryStringExclude', 'string', 'Define parameters to be excluded from the query string (only active if addQueryString is set)', false, '');
$this->registerArgument('absolute', 'bool', 'Ensure the resulting URL is an absolute URL', false, false);
$this->registerArgument('partsAs', 'string', 'Variable name containing typoLink parts (if any), defaults to "typoLinkParts"');
$this->registerArgument('parts-as', 'string', 'Only kept for backwards compatibility, use "partsAs" instead');
$this->registerArgument('textWrap', 'string', 'Wrap the link using the typoscript "wrap" data type', false, '');
}
/**
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
public function render(): string
{
$parameter = $this->arguments['parameter'] ?? '';
$partsAs = $this->arguments['partsAs'] ?? $this->arguments['parts-as'] ?? 'typoLinkParts';
if (!$parameter instanceof TypolinkParameter) {
$parameter = TypolinkParameter::createFromTypolinkParts(
is_scalar($parameter) ? $this->typoLinkCodecService->decode((string)$parameter) : []
);
}
// Merge the $parameter with other arguments
$typolinkParameter = TypolinkParameter::createFromTypolinkParts(self::mergeTypoLinkConfiguration($parameter->toArray(), $this->arguments))->toArray();
// expose internal typoLink configuration to Fluid child context
$variableProvider = new ScopedVariableProvider($this->renderingContext->getVariableProvider(), new StandardVariableProvider([$partsAs => $typolinkParameter]));
$this->renderingContext->setVariableProvider($variableProvider);
// If no link has to be rendered, the inner content will be returned as such
$content = (string)$this->renderChildren();
// clean up exposed variables
$this->renderingContext->setVariableProvider($variableProvider->getGlobalVariableProvider());
$typolink = $this->typoLinkCodecService->encode($typolinkParameter);
if ($typolink !== '') {
$request = null;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
$content = self::invokeContentObjectRenderer($this->arguments, $typolink, $content, $request);
}
return $content;
}
private static function invokeContentObjectRenderer(array $arguments, string $typoLinkParameter, string $content, ?ServerRequestInterface $request): string
{
$addQueryString = $arguments['addQueryString'] ?? false;
$addQueryStringExclude = $arguments['addQueryStringExclude'] ?? '';
$absolute = $arguments['absolute'] ?? false;
$aTagParams = self::serializeTagParameters($arguments);
$instructions = [
'parameter' => $typoLinkParameter,
'ATagParams' => $aTagParams,
'forceAbsoluteUrl' => $absolute,
];
if (array_key_exists('language', $arguments) && $arguments['language'] !== null) {
$instructions['language'] = (string)$arguments['language'];
}
if ($addQueryString && $addQueryString !== 'false') {
$instructions['addQueryString'] = $addQueryString;
$instructions['addQueryString.'] = [
'exclude' => $addQueryStringExclude,
];
}
if ((string)($arguments['textWrap'] ?? '') !== '') {
$instructions['ATagBeforeWrap'] = true;
$instructions['wrap'] = $arguments['textWrap'];
}
$contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
if ($request) {
$contentObject->setRequest($request);
}
return $contentObject->typoLink($content, $instructions);
}
private static function serializeTagParameters(array $arguments): string
{
// array(param1 -> value1, param2 -> value2) --> param1="value1" param2="value2" for typolink.ATagParams
$extraAttributes = [];
$additionalAttributes = $arguments['additionalAttributes'] ?? [];
foreach ($additionalAttributes as $attributeName => $attributeValue) {
$extraAttributes[] = $attributeName . '="' . htmlspecialchars((string)$attributeValue) . '"';
}
return implode(' ', $extraAttributes);
}
/**
* Merges view helper arguments with typolink parts.
*/
private static function mergeTypoLinkConfiguration(array $typoLinkConfiguration, array $arguments): array
{
if ($typoLinkConfiguration === []) {
return $typoLinkConfiguration;
}
$target = $arguments['target'] ?? '';
$class = $arguments['class'] ?? '';
$title = $arguments['title'] ?? '';
$additionalParams = $arguments['additionalParams'] ?? '';
// Override target if given in target argument
if ($target) {
$typoLinkConfiguration['target'] = $target;
}
// Combine classes if given in both "parameter" string and "class" argument
if ($class) {
$classes = explode(' ', trim($typoLinkConfiguration['class']) . ' ' . trim($class));
$typoLinkConfiguration['class'] = implode(' ', array_unique(array_filter($classes)));
}
// Override title if given in title argument
if ($title) {
$typoLinkConfiguration['title'] = $title;
}
// Combine additionalParams
if ($additionalParams) {
$typoLinkConfiguration['additionalParams'] .= $additionalParams;
}
return $typoLinkConfiguration;
}
}
+169
View File
@@ -0,0 +1,169 @@
<?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\Fluid\ViewHelpers;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\Rendering\RendererRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to render a given media file (audio/video/images) with the correct HTML tag.
*
* It utilizes the `RendererRegistry` to determine the correct Renderer class. When no
* renderer can be resolved, it will fall back to use the default `ImageViewHelper`
* for regular images.
*
* ```
* <f:media file="{file}" width="400" height="375" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-media
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-image
* @see RendererRegistry
*/
final class MediaViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'img';
public function __construct(
private readonly RendererRegistry $rendererRegistry,
private readonly ImageService $imageService
) {
parent::__construct();
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('file', 'object', 'File', true);
$this->registerArgument('additionalConfig', 'array', 'This array can hold additional configuration that is passed though to the Renderer object', false, []);
$this->registerArgument('width', 'string', 'This can be a numeric value representing the fixed width of in pixels. But you can also perform simple calculations by adding "m" or "c" to the value. See imgResource.width for possible options.');
$this->registerArgument('height', 'string', 'This can be a numeric value representing the fixed height in pixels. But you can also perform simple calculations by adding "m" or "c" to the value. See imgResource.width for possible options.');
$this->registerArgument('cropVariant', 'string', 'select a cropping variant, in case multiple croppings have been specified or stored in FileReference', false, 'default');
$this->registerArgument('fileExtension', 'string', 'Custom file extension to use for images');
$this->registerArgument('loading', 'string', 'Native lazy-loading for images property. Can be "lazy", "eager" or "auto". Used on image files only.');
$this->registerArgument('decoding', 'string', 'Provides an image decoding hint to the browser. Can be "sync", "async" or "auto"');
}
/**
* Render a given media file.
*/
public function render(): string
{
$file = $this->arguments['file'] ?? null;
$additionalConfig = (array)($this->arguments['additionalConfig'] ?? []);
$width = ($this->arguments['width'] ?? 0);
$height = ($this->arguments['height'] ?? 0);
// get Resource Object (non ExtBase version)
if (is_callable([$file, 'getOriginalResource'])) {
// We have a domain model, so we need to fetch the FAL resource object from there
$file = $file->getOriginalResource();
}
if (!$file instanceof FileInterface) {
throw new InvalidArgumentValueException('Supplied file object type ' . get_class($file) . ' must be FileInterface.', 1454252193);
}
if ((string)($this->arguments['fileExtension'] ?? '') && !GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], (string)$this->arguments['fileExtension'])) {
throw new InvalidArgumentValueException(
'The extension ' . $this->arguments['fileExtension'] . ' is not specified in $GLOBALS[\'TYPO3_CONF_VARS\'][\'GFX\'][\'imagefile_ext\']'
. ' as a valid image file extension and can not be processed.',
1619030957
);
}
$fileRenderer = $this->rendererRegistry->getRenderer($file);
// Fallback to image when no renderer is found
if ($fileRenderer === null) {
return $this->renderImage($file, $width, $height, $this->arguments['fileExtension'] ?? null);
}
$arguments = [];
foreach (array_merge($this->arguments, $this->additionalArguments) as $argumentName => $argumentValue) {
// Prevent "null" when given in fluid
if (!empty($argumentValue) && $argumentValue !== 'null') {
$arguments[$argumentName] = $argumentValue;
}
}
$additionalConfig = array_merge_recursive($arguments, $additionalConfig);
return $fileRenderer->render($file, $width, $height, $additionalConfig);
}
/**
* Render img tag
*
* @param string $width
* @param string $height
* @return string Rendered img tag
*/
private function renderImage(FileInterface $image, $width, $height, ?string $fileExtension): string
{
$cropVariant = (string)(($this->arguments['cropVariant'] ?? '') ?: 'default');
$cropString = $image instanceof FileReference ? $image->getProperty('crop') : '';
$cropVariantCollection = CropVariantCollection::create((string)$cropString);
$cropArea = $cropVariantCollection->getCropArea($cropVariant);
$processingInstructions = [
'width' => $width,
'height' => $height,
'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($image),
];
if (!empty($fileExtension)) {
$processingInstructions['fileExtension'] = $fileExtension;
}
$processedImage = $this->imageService->applyProcessingInstructions($image, $processingInstructions);
$imageUri = $this->imageService->getImageUri($processedImage);
if (!$this->tag->hasAttribute('data-focus-area')) {
$focusArea = $cropVariantCollection->getFocusArea($cropVariant);
if (!$focusArea->isEmpty()) {
$this->tag->addAttribute('data-focus-area', (string)$focusArea->makeAbsoluteBasedOnFile($image));
}
}
$this->tag->addAttribute('src', $imageUri);
$this->tag->addAttribute('width', $processedImage->getProperty('width'));
$this->tag->addAttribute('height', $processedImage->getProperty('height'));
if (in_array($this->arguments['loading'] ?? '', ['lazy', 'eager', 'auto'], true)) {
$this->tag->addAttribute('loading', $this->arguments['loading']);
}
if (in_array($this->arguments['decoding'] ?? '', ['sync', 'async', 'auto'], true)) {
$this->tag->addAttribute('decoding', $this->arguments['decoding']);
}
$alt = $image->getProperty('alternative');
$title = $image->getProperty('title');
// The alt-attribute is mandatory to have valid html-code, therefore add it even if it is empty
if (empty($this->additionalArguments['alt'])) {
$this->tag->addAttribute('alt', $alt ?? '');
}
if (empty($this->additionalArguments['title']) && !empty($title)) {
$this->tag->addAttribute('title', $title);
}
return $this->tag->render();
}
}
@@ -0,0 +1,54 @@
<?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\Fluid\ViewHelpers\Page;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to add various content before the closing body-tag of the current page using PageRenderer.
*
* ```
* <f:page.footerData>
* <script>
* var _paq = window._paq = window._paq || [];
* _paq.push(['trackPageView']);
* _paq.push(['enableLinkTracking']);
* (function() {
* var u = "https://your-matomo-domain.example.com/";
* _paq.push(['setTrackerUrl', u + 'matomo.php']);
* _paq.push(['setSiteId', '1']);
* var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
* g.async = true; g.src = u + 'matomo.js'; s.parentNode.insertBefore(g, s);
* })();
* </script>
* </f:page.footerData>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-page-footerdata
*/
final class FooterDataViewHelper extends AbstractViewHelper
{
public function __construct(private readonly PageRenderer $pageRenderer) {}
public function render(): string
{
$this->pageRenderer->addFooterData($this->renderChildren());
return '';
}
}
@@ -0,0 +1,45 @@
<?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\Fluid\ViewHelpers\Page;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to add various content in the head section of the current page using PageRenderer.
*
* ```
* <f:page.headerData>
* <link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin="anonymous">
* <link rel="dns-prefetch" href="//example-cdn.com">
* <link rel="preconnect" href="https://example-cdn.com">
* </f:page.headerData>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-page-headerdata
*/
final class HeaderDataViewHelper extends AbstractViewHelper
{
public function __construct(private readonly PageRenderer $pageRenderer) {}
public function render(): string
{
$this->pageRenderer->addHeaderData($this->renderChildren());
return '';
}
}
@@ -0,0 +1,66 @@
<?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\Fluid\ViewHelpers\Page;
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to set meta tags from Fluid templates.
*
* ```
* <f:page.meta property="description">My page description</f:page.meta>
* <f:page.meta property="og:title">My article title</f:page.meta>
* <f:page.meta property="og:image" subProperties="{width: 1200, height: 630}">/path/to/image.jpg</f:page.meta>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-page-meta
*/
final class MetaViewHelper extends AbstractViewHelper
{
public function __construct(private readonly MetaTagManagerRegistry $metaTagManagerRegistry) {}
public function initializeArguments(): void
{
$this->registerArgument('property', 'string', 'The meta property name (e.g. "description", "og:title")', true);
$this->registerArgument('type', 'string', 'The meta type attribute (name, property, http-equiv). If not set, the appropriate manager will determine the type.');
$this->registerArgument('subProperties', 'array', 'Array of sub-properties for complex meta tags (e.g. og:image width/height)', false, []);
$this->registerArgument('replace', 'bool', 'Replace existing meta tags with the same property', false, false);
}
public function render(): string
{
$property = $this->arguments['property'];
$content = $this->renderChildren();
if ($content === null || $content === '') {
return '';
}
$metaTagManager = $this->metaTagManagerRegistry->getManagerForProperty($property);
$metaTagManager->addProperty(
$property,
(string)$content,
$this->arguments['subProperties'],
$this->arguments['replace'],
$this->arguments['type'] ?? ''
);
return '';
}
}
@@ -0,0 +1,44 @@
<?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\Fluid\ViewHelpers\Page;
use TYPO3\CMS\Core\PageTitle\RecordTitleProvider;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to set the page title from Fluid templates.
*
* ```
* <f:page.title>My Custom Page Title</f:page.title>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-page-title
*/
final class TitleViewHelper extends AbstractViewHelper
{
public function __construct(private readonly RecordTitleProvider $pageTitleProvider) {}
public function render(): string
{
$title = $this->renderChildren();
if ($title !== null) {
$this->pageTitleProvider->setTitle((string)$title);
}
return '';
}
}
@@ -0,0 +1,129 @@
<?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\Fluid\ViewHelpers\Render;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Page\ContentArea;
use TYPO3\CMS\Fluid\Event\ModifyRenderedContentAreaEvent;
use TYPO3Fluid\Fluid\Core\Variables\ScopedVariableProvider;
use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to render a content area as provided by the page-content processor.
* The most common use case is to render all content elements within a column from a
* backend layout.
*
* ```typoscript
* page = PAGE
* page.10 = PAGEVIEW
* page.10.paths.10 = EXT:my_site_package/Resources/Private/Templates/
* ```
*
* ```html
* <f:render.contentArea contentArea="{content.main}" />
* ```
*
* or:
*
* ```html
* {content.main -> f:render.contentArea()}
* ```
*
* or with markup before and after rendered record by using the "recordAs" argument
* in combination with the `<f:render.record> ViewHelper <https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-render-record>`_:
*
* ```html
* <f:render.contentArea contentArea="{content.main}" recordAs="record">
* before {record.fullType}
* <f:render.record record="{record}" />
* after {record.fullType}
* </f:render.contentArea>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-render-contentarea
*/
final class ContentAreaViewHelper extends AbstractViewHelper
{
/**
* @var bool use content as-is
*/
protected $escapeOutput = false;
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('contentArea', ContentArea::class, 'A content area from the page-content processor');
$this->registerArgument('recordAs', 'string', 'Name of the variable to store the current record in, if you want to use it in the before/after content.');
}
public function render(): string
{
// use argument and fallback to renderChildren for inline records.
$contentArea = $this->arguments['contentArea'] ?? $this->renderChildren();
if (!$contentArea instanceof ContentArea) {
throw new InvalidArgumentValueException('The "contentArea" argument must be an instance of ' . ContentArea::class, 1770212183);
}
$result = '';
if ($this->arguments['recordAs'] !== null) {
$globalVariableProvider = $this->renderingContext->getVariableProvider();
foreach ($contentArea->getRecords() as $record) {
$localVariableProvider = new StandardVariableProvider([$this->arguments['recordAs'] => $record]);
$scopedVariableProvider = new ScopedVariableProvider($globalVariableProvider, $localVariableProvider);
$this->renderingContext->setVariableProvider($scopedVariableProvider);
$result .= $this->renderChildren();
}
$this->renderingContext->setVariableProvider($globalVariableProvider);
} else {
foreach ($contentArea->getRecords() as $record) {
$result .= $this->renderingContext->getViewHelperInvoker()->invoke(
RecordViewHelper::class,
[
'record' => $record,
],
$this->renderingContext,
);
}
}
$event = $this->eventDispatcher->dispatch(
new ModifyRenderedContentAreaEvent(
renderedContentArea: $result,
contentArea: $contentArea,
request: $this->getRequest(),
),
);
return $event->getRenderedContentArea();
}
private function getRequest(): ServerRequestInterface
{
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
throw new \RuntimeException('Required request not found in RenderingContext', 1769183896);
}
return $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
}
@@ -0,0 +1,141 @@
<?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\Fluid\ViewHelpers\Render;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\TypoScript\FrontendTypoScript;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\Event\ModifyRenderedRecordEvent;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to render a record object using its TypoScript definition.
* The most common use case is to render a content element, which is
* available as a record object in a Fluid template.
*
* ```html
* <f:render.record record="{record}" />
* ```
*
* or:
*
* ```html
* {record -> f:render.record()}
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-render-record
*/
final class RecordViewHelper extends AbstractViewHelper
{
/**
* @var bool use content as-is
*/
protected $escapeOutput = false;
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
private readonly TimeTracker $timeTracker,
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('record', RecordInterface::class, 'The record to be rendered');
}
public function getContentArgumentName(): string
{
return 'record';
}
public function render(): string
{
$record = $this->renderChildren();
if (!$record instanceof RecordInterface) {
throw new InvalidArgumentValueException('The "record" argument must be an instance of ' . RecordInterface::class, 1770215699);
}
$request = $this->getRequest();
$result = $this->renderRecord($record, $request);
$event = $this->eventDispatcher->dispatch(
new ModifyRenderedRecordEvent(
renderedRecord: $result,
record: $record,
request: $request,
),
);
return $event->getRenderedRecord();
}
private function renderRecord(RecordInterface $record, ServerRequestInterface $request): string
{
$table = $record->getMainType();
$data = $record->getRawRecord()?->toArray(true) ?? $record->toArray();
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObjectRenderer->setRequest($request);
$parent = $request->getAttribute('currentContentObject');
if ($parent instanceof ContentObjectRenderer) {
$contentObjectRenderer->setParent($parent->data, $parent->currentRecord);
}
$contentObjectRenderer->start($data, $table);
$frontendTypoScript = $request->getAttribute('frontend.typoscript');
if (!$frontendTypoScript instanceof FrontendTypoScript || !$frontendTypoScript->hasSetup()) {
throw new \RuntimeException(
'Full TypoScript setup is not available in the current request. The "f:render.record" ViewHelper'
. ' can only be used in Frontend rendering context.',
1781223613
);
}
$setup = $frontendTypoScript->getSetupArray();
if (!isset($setup[$table])) {
throw new InvalidArgumentValueException(
'No Content Object definition found at TypoScript object path "' . $table . '"',
1769184455
);
}
$timeTracker = $this->timeTracker;
if ($timeTracker->LR) {
$timeTracker->push('/f:render.record/', '<' . $table);
}
$timeTracker->incStackPointer();
$content = $contentObjectRenderer->cObjGetSingle($setup[$table], $setup[$table . '.'] ?? [], $table);
$timeTracker->decStackPointer();
if ($timeTracker->LR) {
$timeTracker->pull($content);
}
return $content;
}
private function getRequest(): ServerRequestInterface
{
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
throw new \RuntimeException('Required request not found in RenderingContext', 1769508877);
}
return $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
}
@@ -0,0 +1,200 @@
<?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\Fluid\ViewHelpers\Render;
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\Schema\Field\InputFieldType;
use TYPO3\CMS\Core\Schema\Field\TextFieldType;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
use TYPO3\CMS\Fluid\ViewHelpers\Format\HtmlViewHelper;
use TYPO3\CMS\Frontend\Page\PageInformation;
use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTML;
use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTMLString;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to render content based on records and fields from a TCA schema.
* Handles the processing of both simple and rich text fields. By default,
* accessing a missing field raises an error. Set `optional` to `true` to
* return null instead.
*
* Can also handle extbase models, you still need to provide the field name, not the property name.
*
* ```html
* <f:render.text record="{page}" field="bodytext" />
* {record -> f:render.text(field: 'title')}
* <f:render.text field="subheader">{record}</f:render.text>
* {record -> f:render.text(field: 'subheader', optional: true)}
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-render-text
*/
final class TextViewHelper extends AbstractViewHelper
{
/**
* We need to disable escaping for the children, otherwise extbase models are given as string to the viewHelper.
* AbstractDomainObject has a __toString method, fluid executes it before giving use the object.
* This is a deeper issue in Fluid that we cannot easily resolve.
* This ViewHelper escapes the output itself, so we can safely disable escaping for the children and output.
*/
protected $escapeChildren = false;
protected $escapeOutput = false;
public function __construct(
private readonly TcaSchemaFactory $tcaSchema,
private readonly RecordFactory $recordFactory,
private readonly DataMapFactory $dataMapFactory,
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('record', PageInformation::class . '|' . RecordInterface::class . '|' . DomainObjectInterface::class, 'A Record API Object or extbase model');
$this->registerArgument('field', 'string', 'The database field that should be rendered (even if extbase model is used).', true);
$this->registerArgument('optional', 'boolean', 'If the provided field does not exist in the record, null will be returned.', false, false);
}
public function getContentArgumentName(): string
{
return 'record';
}
public function validateAdditionalArguments(array $arguments): void
{
// This prevents the default Fluid exception from being thrown for this ViewHelper if it's used
// with arguments that aren't defined in initialArguments(). We do this to make it possible for
// extensions to offer additional functionality by overriding this ViewHelper, which sometimes
// requires adding more (most likely optional) arguments to the ViewHelper's definition.
// Note that this is probably not a long-term solution and might change with future TYPO3 major
// versions. Currently, it has minimal impact to template authors and makes things possible
// for extensions that wouldn't be possible otherwise.
}
public function render(): ?UnsafeHTML
{
$input = $this->renderChildren();
$field = $this->arguments['field'];
if ($input instanceof PageInformation) {
$input = $this->recordFactory->createResolvedRecordFromDatabaseRow('pages', $input->getPageRecord());
}
if (!$input instanceof RecordInterface && !$input instanceof DomainObjectInterface) {
throw new InvalidArgumentValueException(
'The record argument must be an instance of ' . PageInformation::class . ' or ' . RecordInterface::class . ' or ' . DomainObjectInterface::class . ' . Given: ' . get_debug_type($input),
1770539910,
);
}
try {
['table' => $table, 'fullType' => $fullType, 'value' => $value] = $this->extractInformation($input, $field);
} catch (RecordPropertyNotFoundException $exception) {
if ($this->arguments['optional']) {
return null;
}
throw new InvalidArgumentValueException($exception->getMessage(), 1775553111, $exception);
}
if (!is_string($value)) {
throw new InvalidArgumentValueException('The value of the field "' . $table . '.' . $field . '" must be a string. Given: ' . get_debug_type($value), 1770321858);
}
$fieldSchema = $this->tcaSchema->get($fullType)->getField($field);
if ($fieldSchema instanceof InputFieldType) {
return new UnsafeHTMLString(htmlspecialchars($value));
}
if ($fieldSchema instanceof TextFieldType) {
if (!$fieldSchema->isRichText()) {
return new UnsafeHTMLString(nl2br(htmlspecialchars($value)));
}
return new UnsafeHTMLString(
$this->renderingContext->getViewHelperInvoker()->invoke(
HtmlViewHelper::class,
[],
$this->renderingContext,
fn() => $value,
),
);
}
throw new InvalidArgumentValueException('The field "' . $table . '.' . $field . '" is not supported. Given: ' . get_debug_type($fieldSchema), 1770618219);
}
/**
* @return array{table: string, fullType: string, value: mixed}
*/
private function extractInformation(RecordInterface|DomainObjectInterface $input, string $field): array
{
if ($input instanceof RecordInterface) {
return [
'table' => $input->getMainType(),
'fullType' => $input->getFullType(),
'value' => $input->get($field) ?? '',
];
}
$dataMap = $this->dataMapFactory->buildDataMap($input::class);
$recordType = $this->getRecordType($input, $dataMap);
return [
'table' => $dataMap->getTableName(),
'fullType' => $dataMap->getTableName() . ($recordType ? '.' . $recordType : ''),
'value' => $this->getResultingValue($input, $dataMap, $field),
];
}
private function getRecordType(DomainObjectInterface $input, DataMap $dataMap): ?string
{
$recordType = $dataMap->getRecordType();
if ($recordType !== null) {
return $recordType;
}
$recordTypeFieldName = $dataMap->getRecordTypeColumnName();
if ($recordTypeFieldName === null) {
return null;
}
foreach ($input->_getProperties() as $propertyName => $value) {
if ($dataMap->getColumnMap($propertyName)?->columnName === $recordTypeFieldName) {
return $value;
}
}
throw new InvalidArgumentValueException('The record type field "' . $recordTypeFieldName . '" does not exist in the given model ' . $input::class . '.', 1771507212);
}
private function getResultingValue(DomainObjectInterface $input, DataMap $dataMap, string $field): mixed
{
foreach ($input->_getProperties() as $propertyName => $value) {
if ($dataMap->getColumnMap($propertyName)?->columnName === $field) {
return $value ?? '';
}
}
throw new RecordPropertyNotFoundException('Could not find the field "' . $field . '" in the given model ' . $input::class . '.', 1771507213);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?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\Fluid\ViewHelpers;
/**
* A ViewHelper to render a section, a partial, a specified section in a partial
* or a delegate ParsedTemplateInterface implementation.
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-render
*/
final class RenderViewHelper extends \TYPO3Fluid\Fluid\ViewHelpers\RenderViewHelper
{
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('debug', 'boolean', 'If true, the admin panel shows debug information if activated,', false, true);
}
}
@@ -0,0 +1,58 @@
<?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\Fluid\ViewHelpers;
use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Type\StaticResourceInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper for creating system resource objects.
*
* ```
* {f:resource(identifier: 'PKG:typo3/cms-indexed-search:Resources/Public/Css/Stylesheet.css') -> f:uri.resource()}
* {styleSheet -> f:resource() -> f:uri.resource()}
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-resource
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-uri-resource
*/
final class ResourceViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly SystemResourceFactory $systemResourceFactory,
) {}
public function initializeArguments(): void
{
$this->registerArgument('identifier', 'string', 'The resource identifier given as argument or child');
}
public function render(): StaticResourceInterface
{
return $this->systemResourceFactory->createResource($this->renderChildren());
}
/**
* Explicitly set argument name to be used as content.
*/
public function getContentArgumentName(): string
{
return 'identifier';
}
}
@@ -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!
*/
namespace TYPO3\CMS\Fluid\ViewHelpers\Sanitize;
use TYPO3\CMS\Core\Html\SanitizerBuilderFactory;
use TYPO3\CMS\Core\Html\SanitizerInitiator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\HtmlSanitizer\Builder\BuilderInterface;
use TYPO3\HtmlSanitizer\Sanitizer;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to pass a given content through `typo3/html-sanitizer` to mitigate potential
* cross-site scripting occurrences. The `build` option by default uses the class
* `TYPO3\CMS\Core\Html\DefaultSanitizerBuilder`, which declares allowed HTML tags,
* attributes and their values.
*
* ```
* <f:sanitize.html>
* <img src="/img.png" class="image" onmouseover="alert(document.location)">
* </f:sanitize.html>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-sanitize-html
* @see \TYPO3\CMS\Core\Html\DefaultSanitizerBuilder
*/
final class HtmlViewHelper extends AbstractViewHelper
{
/**
* @var bool
*/
protected $escapeChildren = false;
/**
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly SanitizerBuilderFactory $sanitizerBuilderFactory
) {}
public function initializeArguments(): void
{
$this->registerArgument('build', 'string', 'preset name or class-like name of sanitization builder', false, 'default');
}
public function render(): string
{
$value = $this->renderChildren();
$build = $this->arguments['build'];
return $this->createSanitizer($build)->sanitize((string)$value, self::createInitiator());
}
private static function createInitiator(): SanitizerInitiator
{
return GeneralUtility::makeInstance(SanitizerInitiator::class, self::class);
}
private function createSanitizer(string $build): Sanitizer
{
if (class_exists($build) && is_a($build, BuilderInterface::class, true)) {
$builder = GeneralUtility::makeInstance($build);
} else {
$builder = $this->sanitizerBuilderFactory->build($build);
}
return $builder->build();
}
}
@@ -0,0 +1,47 @@
<?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\Fluid\ViewHelpers\Security;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper;
/**
* ViewHelper implementing an ifAuthenticated/else condition for frontend users.
*
* ```
* <f:security.ifAuthenticated>
* <f:then>
* This is being shown in case you have access.
* </f:then>
* <f:else>
* This is being displayed in case you do not have access.
* </f:else>
* </f:security.ifAuthenticated>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-security-ifauthenticated
*/
final class IfAuthenticatedViewHelper extends AbstractConditionViewHelper
{
public static function verdict(array $arguments, RenderingContextInterface $renderingContext): bool
{
return GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'id', 0) > 0;
}
}
@@ -0,0 +1,69 @@
<?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\Fluid\ViewHelpers\Security;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper;
/**
* ViewHelper implementing an ifHasRole/else condition for frontend groups.
*
* ```
* <f:security.ifHasRole role="Administrator">
* <f:then>
* This is being shown in case you have the role.
* </f:then>
* <f:else>
* This is being displayed in case you do not have the role.
* </f:else>
* </f:security.ifHasRole>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-security-ifhasrole
*/
final class IfHasRoleViewHelper extends AbstractConditionViewHelper
{
/**
* Initializes the "role" argument.
* Renders <f:then> child if the current logged in FE user belongs to the specified role (aka usergroup)
* otherwise renders <f:else> child.
*/
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('role', 'string', 'The usergroup (either the usergroup uid or its title).');
}
public static function verdict(array $arguments, RenderingContextInterface $renderingContext): bool
{
$role = $arguments['role'];
/** @var UserAspect $userAspect */
$userAspect = GeneralUtility::makeInstance(Context::class)->getAspect('frontend.user');
if (!$userAspect->isLoggedIn()) {
return false;
}
if (is_numeric($role)) {
$groupIds = $userAspect->getGroupIds();
return in_array((int)$role, $groupIds, true);
}
return in_array($role, $userAspect->getGroupNames(), true);
}
}
@@ -0,0 +1,66 @@
<?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\Fluid\ViewHelpers\Security;
use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to retrieve (and consume) a `nonce` attribute from
* the global server request object pool, or from the `PolicyProvider`
* service as a fall-back value.
*
* ```
* <script nonce="{f:security.nonce(directive: 'script-src')}">const inline = 'script';</script>
* <script nonce="{f:security.nonce(directive: 'script-src', scope: 'static')}" src="app.js"></script>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-security-nonce
* @see https://docs.typo3.org/permalink/t3coreapi:content-security-policy
* @see \TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider
*/
final class NonceViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly RequestId $requestId,
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('directive', 'string', 'Value of the CSP directive');
$this->registerArgument('scope', 'string', '`inline` or `static`', false, 'inline');
}
public function render(): string
{
$applicableDirectives = SourceKeyword::nonceProxy->getApplicableDirectives();
$directive = Directive::tryFrom($this->arguments['directive'] ?? '');
$directive = $directive !== null && in_array($directive, $applicableDirectives, true)
? $directive->value
: self::class;
$scope = $this->arguments['scope'] ?? '';
if ($scope === 'static') {
return $this->requestId->nonce->consumeStatic($directive);
}
// `inline` is guessed here, it might be `static` as well in templates
return $this->requestId->nonce->consumeInline($directive);
}
}
@@ -0,0 +1,77 @@
<?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\Fluid\ViewHelpers\Transform;
use TYPO3\CMS\Frontend\Html\HtmlWorker;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to transform HTML and substitute internal link scheme aspects.
*
* ```
* <f:transform.html selector="a.href" onFailure="removeEnclosure">
* <a href="t3://page?uid=1" class="home">Home</a>
* </f:transform.html>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-transform-html
*/
final class HtmlViewHelper extends AbstractViewHelper
{
protected const array MAP_ON_FAILURE = [
'' => 0,
'null' => 0,
'removeTag' => HtmlWorker::REMOVE_TAG_ON_FAILURE,
'removeAttr' => HtmlWorker::REMOVE_ATTR_ON_FAILURE,
'removeEnclosure' => HtmlWorker::REMOVE_ENCLOSURE_ON_FAILURE,
];
/**
* @var bool
*/
protected $escapeChildren = false;
/**
* @var bool
*/
protected $escapeOutput = false;
public function __construct(
private readonly HtmlWorker $htmlWorker
) {}
public function initializeArguments(): void
{
$this->registerArgument('selector', 'string', 'comma separated list of node attributes to be considered', false, 'a.href');
$this->registerArgument('onFailure', 'string', 'behavior on failure, either `removeTag`, `removeAttr`, `removeEnclosure` or `null`', false, 'removeEnclosure');
}
/**
* @return string transformed markup
*/
public function render(): string
{
$content = $this->renderChildren();
$selector = $this->arguments['selector'];
$onFailure = $this->arguments['onFailure'];
$onFailureFlags = self::MAP_ON_FAILURE[$onFailure] ?? HtmlWorker::REMOVE_ENCLOSURE_ON_FAILURE;
return (string)$this->htmlWorker
->parse((string)$content)
->transformUri($selector, $onFailureFlags);
}
}
+191
View File
@@ -0,0 +1,191 @@
<?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\Fluid\ViewHelpers;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\Locale;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Localization\TranslationDomainMapper;
use TYPO3\CMS\Core\Localization\TranslationDomainResolver;
use TYPO3\CMS\Extbase\Mvc\RequestInterface as ExtbaseRequestInterface;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
use TYPO3Fluid\Fluid\Core\ViewHelper\MissingArgumentException;
/**
* ViewHelper to provide a translation for language keys ("locallang"/"LLL").
* By default, the files are loaded from the folder `Resources/Private/Language/`.
*
* Supports two placeholder formats:
*
* 1. ICU MessageFormat with named arguments (for labels like "{count, plural, one {# file} other {# files}}"):
* ```
* <f:translate key="file_count" arguments="{count: fileCount}" />
* ```
*
* 2. sprintf-style with positional arguments (for labels like "Downloaded %d times from %s"):
* ```
* <f:translate key="someKey" arguments="{0: 42, 1: 'server'}" />
* ```
*
* Example usage:
* ```
* <f:translate key="LLL:EXT:myext/Resources/Private/Language/locallang.xlf:key1" />
* <f:translate key="items_count" arguments="{count: items.count}" />
* <f:translate key="someKey" arguments="{0: 'dog', 1: 'fox'}" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-translate
* @see https://php.net/sprintf
* @see https://unicode-org.github.io/icu/userguide/format_parse/messages/
*/
final class TranslateViewHelper extends AbstractViewHelper
{
/**
* Output is escaped already. We must not escape children, to avoid double encoding.
*
* @var bool
*/
protected $escapeChildren = false;
public function __construct(
private readonly TranslationDomainMapper $translationDomainMapper,
private readonly TranslationDomainResolver $translationDomainResolver,
private readonly Locales $locales,
) {}
public function initializeArguments(): void
{
$this->registerArgument('key', 'string', 'Translation Key');
$this->registerArgument('id', 'string', 'Translation ID. Same as key.');
$this->registerArgument('default', 'string', 'If the given locallang key could not be found, this value is used. If this argument is not set, child nodes will be used to render the default');
$this->registerArgument('arguments', 'array', 'Arguments to be replaced in the resulting string');
$this->registerArgument('extensionName', 'string', 'UpperCamelCased extension key (for example BlogExample)');
$this->registerArgument('domain', 'string', 'Translation Domain to be used for the ID/Key. Takes precedence over "extensionName". Should also be used over "extensionName".');
$this->registerArgument('languageKey', 'string', 'Language key ("da" for example) or "en" to use. Also a Locale object is possible. If empty, use current locale from the request.');
}
/**
* Return array element by key.
*/
public function render(): string
{
$key = $this->arguments['key'];
$id = $this->arguments['id'];
$default = (string)($this->arguments['default'] ?? $this->renderChildren() ?? '');
$domain = $this->arguments['domain'];
$extensionName = $this->arguments['extensionName'];
$translateArguments = $this->arguments['arguments'];
// Use key if id is empty.
if ($id === null) {
$id = $key;
}
$id = (string)$id;
if ($id === '') {
throw new MissingArgumentException('An argument "key" or "id" has to be provided', 1351584844);
}
$request = null;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
// If a domain is given, it takes precedence over extensionName
if (!empty($domain)) {
$extensionName = $domain;
} elseif (empty($extensionName)) {
if (str_starts_with($id, 'LLL:EXT:')) {
$extensionName = substr($id, 8, strpos($id, '/', 8) - 8);
} elseif (str_starts_with($id, 'LLL:')) {
// Implicit domain usage, let's keep it as is
[$prefix, $domain, $id] = explode(':', $id, 3);
$extensionName = $domain;
} elseif (str_contains($id, ':')) {
// Check if the domain name is actually valid.
[
$possibleDomain,
$possibleId
] = explode(':', $id, 2);
if ($this->translationDomainResolver->isValidDomainName($possibleDomain) && $this->translationDomainMapper->mapDomainToFileName($possibleDomain) !== $possibleDomain) {
$extensionName = $possibleDomain;
$id = $possibleId;
}
} elseif ($request instanceof ExtbaseRequestInterface) {
$extensionName = $request->getControllerExtensionName();
} else {
if ($default) {
return $this->handleDefaultValue($default, $translateArguments, $request);
}
}
}
if (empty($extensionName) && empty($default)) {
// Throw exception in case neither an extension key nor a extbase request
// are given, since the "short key" shouldn't be considered as a label.
throw new Exception(
'ViewHelper f:translate in non-extbase context needs attribute "domain" or "extensionName" to resolve'
. ' key="' . $id . '" without path. Either set attribute "domain" or "extensionName" together with the short'
. ' key "yourKey" to result'
. ' or (better) use a full LLL reference like key="LLL:your_extension.name:yourKey".'
. ' Alternatively, you can also define a default value.',
1639828178
);
}
try {
$locale = $this->getUsedLocale($this->arguments['languageKey'], $request);
$value = LocalizationUtility::translate($id, $extensionName, $translateArguments, $locale, $request);
} catch (\InvalidArgumentException) {
// @todo: Switch to more specific Exceptions here - for instance those thrown when a package was not found, see #95957
$value = null;
}
if ($value === null) {
return $this->handleDefaultValue($default, $translateArguments, $request);
}
return $value;
}
/**
* Ensure that a string is returned, if the underlying logic returns null, or cannot handle a translation
*/
private function handleDefaultValue(string $default, ?array $translateArguments, ?ServerRequestInterface $request = null): string
{
if (!empty($translateArguments)) {
// Check for ICU pattern markers
if (array_is_list($translateArguments)) {
return vsprintf($default, $translateArguments);
}
$locale = $request ? $this->locales->createLocaleFromRequest($request)->posixFormatted() : 'en_US';
$formatted = \MessageFormatter::formatMessage($locale, $default, $translateArguments);
if ($formatted !== false) {
return $formatted;
}
}
return $default;
}
private function getUsedLocale(Locale|string|null $languageKey, ?ServerRequestInterface $request): Locale|string|null
{
if ($languageKey !== null && $languageKey !== '') {
return $languageKey;
}
if ($request) {
return $this->locales->createLocaleFromRequest($request);
}
return null;
}
}
@@ -0,0 +1,243 @@
<?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\Fluid\ViewHelpers\Uri;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\RequestInterface as ExtbaseRequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder as ExtbaseUriBuilder;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Typolink\LinkFactory;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\MissingArgumentException;
/**
* ViewHelper for creating URIs to Extbase actions (within Controllers).
* Tailored for Extbase plugins, uses Extbase Request and Extbase UriBuilder.
*
* ```
* <f:uri.action action="show" arguments="{blog: blog.uid}">action link</f:uri.action>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-uri-action
*/
final class ActionViewHelper extends AbstractViewHelper
{
public function initializeArguments(): void
{
$this->registerArgument('action', 'string', 'Target action');
$this->registerArgument('arguments', 'array', 'Arguments for the controller action, associative array (do not use reserved keywords "action", "controller" or "format" if not referring to these internal variables specifically)', false, []);
$this->registerArgument('controller', 'string', 'Target controller. If NULL current controllerName is used');
$this->registerArgument('extensionName', 'string', 'Target Extension Name (without `tx_` prefix and no underscores). If NULL the current extension name is used');
$this->registerArgument('pluginName', 'string', 'Target plugin. If empty, the current plugin name is used');
$this->registerArgument('pageUid', 'int', 'Target page. See TypoLink destination');
$this->registerArgument('pageType', 'int', 'Type of the target page. See typolink.parameter', false, 0);
$this->registerArgument('noCache', 'bool', 'Set this to disable caching for the target page. You should not need this.');
$this->registerArgument('language', 'string', 'link to a specific language - defaults to the current language, use a language ID or "current" to enforce a specific language');
$this->registerArgument('section', 'string', 'The anchor to be added to the URI', false, '');
$this->registerArgument('format', 'string', 'The requested format, e.g. ".html', false, '');
$this->registerArgument('linkAccessRestrictedPages', 'bool', 'If set, links pointing to access restricted pages will still link to the page even though the page cannot be accessed.', false, false);
$this->registerArgument('additionalParams', 'array', 'Additional query parameters that won\'t be prefixed like $arguments (overrule $arguments)', false, []);
$this->registerArgument('absolute', 'bool', 'If set, the URI of the rendered link is absolute', false, false);
$this->registerArgument('addQueryString', 'string', 'If set, the current query parameters will be kept in the URL. If set to "untrusted", then ALL query parameters will be added. Be aware, that this might lead to problems when the generated link is cached.', false, false);
$this->registerArgument('argumentsToBeExcludedFromQueryString', 'array', 'Arguments to be removed from the URI. Only active if $addQueryString = true', false, []);
}
public function render(): string
{
$request = null;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
$childContent = (string)$this->renderChildren();
if ($request instanceof ExtbaseRequestInterface) {
$uri = self::createUriWithExtbaseContext($request, $this->arguments);
if ($uri === '') {
return $childContent;
}
return $uri;
}
if ($request instanceof ServerRequestInterface && ApplicationType::fromRequest($request)->isFrontend()) {
$linkResult = self::createFrontendLinkWithCoreContext($request, $this->arguments, $childContent);
if ($linkResult === null) {
return $childContent;
}
return $linkResult->getUrl();
}
throw new \RuntimeException(
'The rendering context of ViewHelper f:uri.action is missing a valid request object.',
1690360598
);
}
/**
* Only to be used by \TYPO3\CMS\Fluid\ViewHelpers\Link\ActionViewHelper
* @internal
*/
public static function createFrontendLinkWithCoreContext(ServerRequestInterface $request, array $arguments, string $childContent): ?LinkResultInterface
{
// No support for following arguments:
// * format
$pageUid = isset($arguments['pageUid']) ? (int)$arguments['pageUid'] : null;
$pageType = (int)$arguments['pageType'];
$noCache = (bool)($arguments['noCache'] ?? false);
$language = isset($arguments['language']) ? (string)$arguments['language'] : null;
$section = $arguments['section'];
$linkAccessRestrictedPages = (bool)$arguments['linkAccessRestrictedPages'];
$additionalParams = (array)$arguments['additionalParams'];
$absolute = (bool)$arguments['absolute'];
/** @var bool|string $addQueryString */
$addQueryString = $arguments['addQueryString'];
$argumentsToBeExcludedFromQueryString = (array)$arguments['argumentsToBeExcludedFromQueryString'];
/** @var string|null $action */
$action = $arguments['action'];
/** @var string|null $controller */
$controller = $arguments['controller'];
/** @var string|null $extensionName */
$extensionName = $arguments['extensionName'];
/** @var string|null $pluginName */
$pluginName = $arguments['pluginName'];
$actionArguments = (array)$arguments['arguments'];
$allExtbaseArgumentsAreSet = (
is_string($extensionName) && $extensionName !== ''
&& is_string($pluginName) && $pluginName !== ''
&& is_string($controller) && $controller !== ''
&& is_string($action) && $action !== ''
);
if (!$allExtbaseArgumentsAreSet) {
throw new MissingArgumentException(
'ViewHelper f:link.action / f:uri.action needs either all extbase arguments set'
. ' ("extensionName", "pluginName", "controller", "action")'
. ' or needs a request implementing extbase RequestInterface.',
1690370264
);
}
// Provide extbase default and custom arguments as prefixed additional params
$extbaseArgumentNamespace = 'tx_'
. str_replace('_', '', strtolower($extensionName))
. '_'
. str_replace('_', '', strtolower($pluginName));
$additionalParams[$extbaseArgumentNamespace] = array_replace(
[
'controller' => $controller,
'action' => $action,
],
$actionArguments
);
$typolinkConfiguration = [
'parameter' => $pageUid ?: 'current',
];
if ($pageType) {
$typolinkConfiguration['parameter'] .= ',' . $pageType;
}
if ($language !== null) {
$typolinkConfiguration['language'] = $language;
}
if ($noCache) {
$typolinkConfiguration['no_cache'] = 1;
}
if ($section) {
$typolinkConfiguration['section'] = $section;
}
if ($linkAccessRestrictedPages) {
$typolinkConfiguration['linkAccessRestrictedPages'] = 1;
}
$typolinkConfiguration['queryParameters'] = $additionalParams;
if ($absolute) {
$typolinkConfiguration['forceAbsoluteUrl'] = true;
}
if ($addQueryString && $addQueryString !== 'false') {
$typolinkConfiguration['addQueryString'] = $addQueryString;
if ($argumentsToBeExcludedFromQueryString !== []) {
$typolinkConfiguration['addQueryString.']['exclude'] = implode(',', $argumentsToBeExcludedFromQueryString);
}
}
try {
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$linkFactory = GeneralUtility::makeInstance(LinkFactory::class);
return $linkFactory->create($childContent, $typolinkConfiguration, $cObj);
} catch (UnableToLinkException) {
return null;
}
}
/**
* Only to be used by \TYPO3\CMS\Fluid\ViewHelpers\Link\ActionViewHelper
* @internal
*/
public static function createUriWithExtbaseContext(ExtbaseRequestInterface $request, array $arguments): string
{
$format = $arguments['format'];
$pageUid = (int)($arguments['pageUid'] ?? 0);
$pageType = (int)$arguments['pageType'];
$noCache = (bool)($arguments['noCache'] ?? false);
$language = isset($arguments['language']) ? (string)$arguments['language'] : null;
$section = $arguments['section'];
$linkAccessRestrictedPages = (bool)$arguments['linkAccessRestrictedPages'];
$additionalParams = (array)$arguments['additionalParams'];
$absolute = (bool)$arguments['absolute'];
/** @var bool|string $addQueryString */
$addQueryString = $arguments['addQueryString'];
$argumentsToBeExcludedFromQueryString = (array)$arguments['argumentsToBeExcludedFromQueryString'];
/** @var string|null $action */
$action = $arguments['action'];
/** @var string|null $controller */
$controller = $arguments['controller'];
/** @var string|null $extensionName */
$extensionName = $arguments['extensionName'];
/** @var string|null $pluginName */
$pluginName = $arguments['pluginName'];
$actionArguments = (array)$arguments['arguments'];
$uriBuilder = GeneralUtility::makeInstance(ExtbaseUriBuilder::class);
$uriBuilder
->reset()
->setRequest($request)
->setNoCache($noCache)
->setLanguage($language)
->setSection($section)
->setFormat($format)
->setLinkAccessRestrictedPages($linkAccessRestrictedPages)
->setArguments($additionalParams)
->setCreateAbsoluteUri($absolute);
if ($addQueryString && $addQueryString !== 'false') {
$uriBuilder
->setAddQueryString($addQueryString)
->setArgumentsToBeExcludedFromQueryString($argumentsToBeExcludedFromQueryString);
}
if ($pageUid > 0) {
$uriBuilder->setTargetPageUid($pageUid);
}
if ($pageType > 0) {
$uriBuilder->setTargetPageType($pageType);
}
return $uriBuilder->uriFor($action, $actionArguments, $controller, $extensionName, $pluginName);
}
}
@@ -0,0 +1,51 @@
<?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\Fluid\ViewHelpers\Uri;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper for creating URIs to external targets, enforcing a specific scheme
* (https by default).
* The specified URI is passed through without further resolving or transformation.
*
* ```
* <f:uri.external uri="https://www.typo3.org" target="_blank">external link</f:uri.external>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-uri-external
*/
final class ExternalViewHelper extends AbstractViewHelper
{
public function initializeArguments(): void
{
$this->registerArgument('uri', 'string', 'target URI', true);
$this->registerArgument('defaultScheme', 'string', 'scheme the href attribute will be prefixed with if specified $uri does not contain a scheme already', false, 'https');
}
public function render(): string
{
$uri = $this->arguments['uri'];
$defaultScheme = $this->arguments['defaultScheme'];
$scheme = parse_url($uri, PHP_URL_SCHEME);
if ($scheme === null && $defaultScheme !== '') {
$uri = $defaultScheme . '://' . $uri;
}
return $uri;
}
}
+169
View File
@@ -0,0 +1,169 @@
<?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\Fluid\ViewHelpers\Uri;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to resize, crop or convert a given image (if required) and return
* the URL to this processed file.
*
* This ViewHelper should only be used for images within FAL storages,
* or where graphical operations shall be performed.
*
* Note that when the contents of a non-FAL image are changed,
* an image may not show updated processed contents unless either the
* FAL record is updated/removed, or the temporary processed images are
* cleared.
*
* Also note that image operations (cropping, scaling, converting) on
* non-FAL files may be changed in future TYPO3 versions, since those operations
* are coupled with FAL metadata. Each non-FAL image operation creates a
* "fake" FAL record, which may lead to problems.
*
* For extension resource files, use `<f:uri.resource>` instead.
*
* External URLs are not processed and just returned as is.
*
* ```
* <f:uri.image src="{variableWithFileadminLocation}" width="100c" />
* <f:uri.image image="{imageObject}" maxWidth="400" maxHeight="400" fileExtension="webp" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-uri-image
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-uri-resource
*/
final class ImageViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly ImageService $imageService
) {}
public function initializeArguments(): void
{
$this->registerArgument('src', 'string', 'src', false, '');
$this->registerArgument('treatIdAsReference', 'bool', 'given src argument is a sys_file_reference record', false, false);
$this->registerArgument('image', 'object', 'image');
$this->registerArgument('crop', 'string|bool|array', 'overrule cropping of image (setting to FALSE disables the cropping set in FileReference)');
$this->registerArgument('cropVariant', 'string', 'select a cropping variant, in case multiple croppings have been specified or stored in FileReference', false, 'default');
$this->registerArgument('fileExtension', 'string', 'Custom file extension to use');
$this->registerArgument('width', 'string', 'width of the image. This can be a numeric value representing the fixed width of the image in pixels. But you can also perform simple calculations by adding "m" or "c" to the value. See imgResource.width for possible options.');
$this->registerArgument('height', 'string', 'height of the image. This can be a numeric value representing the fixed height of the image in pixels. But you can also perform simple calculations by adding "m" or "c" to the value. See imgResource.width for possible options.');
$this->registerArgument('minWidth', 'int', 'minimum width of the image');
$this->registerArgument('minHeight', 'int', 'minimum height of the image');
$this->registerArgument('maxWidth', 'int', 'maximum width of the image');
$this->registerArgument('maxHeight', 'int', 'maximum height of the image');
$this->registerArgument('absolute', 'bool', 'Force absolute URL', false, false);
$this->registerArgument('base64', 'bool', 'Return a base64 encoded version of the image', false, false);
}
/**
* Resizes the image (if required) and returns its path. If the image was not resized, the path will be equal to $src
*/
public function render(): string
{
$src = (string)$this->arguments['src'];
$image = $this->arguments['image'];
$treatIdAsReference = (bool)$this->arguments['treatIdAsReference'];
$cropString = $this->arguments['crop'];
$absolute = $this->arguments['absolute'];
if (($src === '' && $image === null) || ($src !== '' && $image !== null)) {
throw new InvalidArgumentValueException(self::getExceptionMessage('You must either specify a string src or a File object.', $this->renderingContext), 1460976233);
}
if ((string)$this->arguments['fileExtension'] && !GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], (string)$this->arguments['fileExtension'])) {
throw new InvalidArgumentValueException(
self::getExceptionMessage(
'The extension ' . $this->arguments['fileExtension'] . ' is not specified in $GLOBALS[\'TYPO3_CONF_VARS\'][\'GFX\'][\'imagefile_ext\']'
. ' as a valid image file extension and can not be processed.',
$this->renderingContext
),
1618992262
);
}
try {
$image = $this->imageService->getImage($src, $image, $treatIdAsReference);
if ($cropString === null && $image->hasProperty('crop') && $image->getProperty('crop')) {
$cropString = $image->getProperty('crop');
}
// CropVariantCollection needs a string, but this VH could also receive an array
if (is_array($cropString)) {
$cropString = json_encode($cropString);
}
$cropVariantCollection = CropVariantCollection::create((string)$cropString);
$cropVariant = $this->arguments['cropVariant'] ?: 'default';
$cropArea = $cropVariantCollection->getCropArea($cropVariant);
$processingInstructions = [
'width' => $this->arguments['width'],
'height' => $this->arguments['height'],
'minWidth' => $this->arguments['minWidth'],
'minHeight' => $this->arguments['minHeight'],
'maxWidth' => $this->arguments['maxWidth'],
'maxHeight' => $this->arguments['maxHeight'],
'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($image),
];
if (!empty($this->arguments['fileExtension'])) {
$processingInstructions['fileExtension'] = $this->arguments['fileExtension'];
}
$processedImage = $this->imageService->applyProcessingInstructions($image, $processingInstructions);
if ($this->arguments['base64']) {
return 'data:' . $processedImage->getMimeType() . ';base64,' . base64_encode($processedImage->getContents());
}
return $this->imageService->getImageUri($processedImage, $absolute);
} catch (ResourceDoesNotExistException $e) {
// thrown if file does not exist
throw new Exception(self::getExceptionMessage($e->getMessage(), $this->renderingContext), 1509741907, $e);
} catch (\UnexpectedValueException $e) {
// thrown if a file has been replaced with a folder
throw new Exception(self::getExceptionMessage($e->getMessage(), $this->renderingContext), 1509741908, $e);
} catch (\InvalidArgumentException $e) {
// thrown if file storage does not exist
throw new Exception(self::getExceptionMessage($e->getMessage(), $this->renderingContext), 1509741910, $e);
}
}
private static function getExceptionMessage(string $detailedMessage, RenderingContextInterface $renderingContext): string
{
$request = null;
if ($renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $renderingContext->getAttribute(ServerRequestInterface::class);
}
if ($request instanceof RequestInterface) {
$currentContentObject = $request->getAttribute('currentContentObject');
if ($currentContentObject instanceof ContentObjectRenderer) {
return sprintf('Unable to render image URI in "%s": %s', $currentContentObject->currentRecord, $detailedMessage);
}
}
return sprintf('Unable to render image URI: %s', $detailedMessage);
}
}
+218
View File
@@ -0,0 +1,218 @@
<?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\Fluid\ViewHelpers\Uri;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Routing\UriBuilder as BackendUriBuilder;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Extbase\Mvc\RequestInterface as ExtbaseRequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder as ExtbaseUriBuilder;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Typolink\LinkFactory;
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper for creating URIs to TYPO3 pages.
*
* ```
* <f:uri.page pageUid="42" additionalParams="{foo: 'bar'}">page link</f:uri.page>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-uri-page
*/
final class PageViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly BackendUriBuilder $backendUriBuilder,
private readonly LinkFactory $linkFactory,
) {}
public function initializeArguments(): void
{
$this->registerArgument('pageUid', 'int', 'target PID');
$this->registerArgument('additionalParams', 'array', 'query parameters to be attached to the resulting URI', false, []);
$this->registerArgument('pageType', 'int', 'type of the target page. See typolink.parameter', false, 0);
$this->registerArgument('noCache', 'bool', 'set this to disable caching for the target page. You should not need this.', false, false);
$this->registerArgument('language', 'string', 'link to a specific language - defaults to the current language, use a language ID or "current" to enforce a specific language');
$this->registerArgument('section', 'string', 'the anchor to be added to the URI', false, '');
$this->registerArgument('linkAccessRestrictedPages', 'bool', 'If set, links pointing to access restricted pages will still link to the page even though the page cannot be accessed.', false, false);
$this->registerArgument('absolute', 'bool', 'If set, the URI of the rendered link is absolute', false, false);
$this->registerArgument('addQueryString', 'string', 'If set, the current query parameters will be kept in the URL. If set to "untrusted", then ALL query parameters will be added. Be aware, that this might lead to problems when the generated link is cached.', false, false);
$this->registerArgument('argumentsToBeExcludedFromQueryString', 'array', 'arguments to be removed from the URI. Only active if $addQueryString = TRUE', false, []);
}
public function render(): string
{
$request = null;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
if ($request instanceof ExtbaseRequestInterface) {
return self::renderWithExtbaseContext($request, $this->arguments);
}
if ($request instanceof ServerRequestInterface) {
if (ApplicationType::fromRequest($request)->isFrontend()) {
// Use the regular typolink functionality.
return $this->renderFrontendLinkWithCoreContext($request, $this->arguments, $this->renderChildren(...));
}
return $this->renderBackendLinkWithCoreContext($request, $this->arguments);
}
throw new \RuntimeException(
'The rendering context of ViewHelper f:uri.page is missing a valid request object.',
1639820200
);
}
private function renderBackendLinkWithCoreContext(ServerRequestInterface $request, array $arguments): string
{
$pageUid = isset($arguments['pageUid']) ? (int)$arguments['pageUid'] : null;
$section = isset($arguments['section']) ? (string)$arguments['section'] : '';
$additionalParams = isset($arguments['additionalParams']) ? (array)$arguments['additionalParams'] : [];
$absolute = isset($arguments['absolute']) && (bool)$arguments['absolute'];
$addQueryString = $arguments['addQueryString'] ?? false;
$argumentsToBeExcludedFromQueryString = isset($arguments['argumentsToBeExcludedFromQueryString']) ? (array)$arguments['argumentsToBeExcludedFromQueryString'] : [];
$arguments = [];
if ($addQueryString && $addQueryString !== 'false') {
$arguments = $request->getQueryParams();
foreach ($argumentsToBeExcludedFromQueryString as $argumentToBeExcluded) {
$argumentArrayToBeExcluded = [];
parse_str($argumentToBeExcluded, $argumentArrayToBeExcluded);
$arguments = ArrayUtility::arrayDiffKeyRecursive($arguments, $argumentArrayToBeExcluded);
}
}
$id = $pageUid ?? $request->getQueryParams()['id'] ?? null;
if ($id !== null) {
$arguments['id'] = $id;
}
if (!isset($arguments['route']) && ($route = $request->getAttribute('route')) instanceof Route) {
$arguments['route'] = $route->getOption('_identifier');
}
$arguments = array_replace_recursive($arguments, $additionalParams);
$routeName = $arguments['route'] ?? null;
unset($arguments['route'], $arguments['token']);
try {
if ($absolute) {
$uri = (string)$this->backendUriBuilder->buildUriFromRoute($routeName, $arguments, BackendUriBuilder::ABSOLUTE_URL);
} else {
$uri = (string)$this->backendUriBuilder->buildUriFromRoute($routeName, $arguments, BackendUriBuilder::ABSOLUTE_PATH);
}
} catch (RouteNotFoundException) {
$uri = '';
}
if ($section !== '') {
$uri .= '#' . $section;
}
return $uri;
}
private function renderFrontendLinkWithCoreContext(ServerRequestInterface $request, array $arguments, \Closure $renderChildrenClosure): string
{
$pageUid = isset($arguments['pageUid']) ? (int)$arguments['pageUid'] : 'current';
$pageType = isset($arguments['pageType']) ? (int)$arguments['pageType'] : 0;
$noCache = isset($arguments['noCache']) && (bool)$arguments['noCache'];
$section = isset($arguments['section']) ? (string)$arguments['section'] : '';
$language = isset($arguments['language']) ? (string)$arguments['language'] : null;
$linkAccessRestrictedPages = isset($arguments['linkAccessRestrictedPages']) && (bool)$arguments['linkAccessRestrictedPages'];
$additionalParams = isset($arguments['additionalParams']) ? (array)$arguments['additionalParams'] : [];
$absolute = isset($arguments['absolute']) && (bool)$arguments['absolute'];
$addQueryString = $arguments['addQueryString'] ?? false;
$argumentsToBeExcludedFromQueryString = isset($arguments['argumentsToBeExcludedFromQueryString']) ? (array)$arguments['argumentsToBeExcludedFromQueryString'] : [];
$typolinkConfiguration = [
'parameter' => $pageUid,
];
if ($pageType) {
$typolinkConfiguration['parameter'] .= ',' . $pageType;
}
if ($noCache) {
$typolinkConfiguration['no_cache'] = 1;
}
if ($language !== null) {
$typolinkConfiguration['language'] = $language;
}
if ($section) {
$typolinkConfiguration['section'] = $section;
}
if ($linkAccessRestrictedPages) {
$typolinkConfiguration['linkAccessRestrictedPages'] = 1;
}
if ($additionalParams) {
$typolinkConfiguration['queryParameters'] = $additionalParams;
}
if ($absolute) {
$typolinkConfiguration['forceAbsoluteUrl'] = true;
}
if ($addQueryString && $addQueryString !== 'false') {
$typolinkConfiguration['addQueryString'] = $addQueryString;
if ($argumentsToBeExcludedFromQueryString !== []) {
$typolinkConfiguration['addQueryString.']['exclude'] = implode(',', $argumentsToBeExcludedFromQueryString);
}
}
try {
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$linkResult = $this->linkFactory->create((string)$renderChildrenClosure(), $typolinkConfiguration, $cObj);
return $linkResult->getUrl();
} catch (UnableToLinkException) {
return (string)$renderChildrenClosure();
}
}
private static function renderWithExtbaseContext(ExtbaseRequestInterface $request, array $arguments): string
{
$pageUid = $arguments['pageUid'];
$additionalParams = $arguments['additionalParams'];
$pageType = (int)($arguments['pageType'] ?? 0);
$noCache = $arguments['noCache'];
$section = $arguments['section'];
$language = isset($arguments['language']) ? (string)$arguments['language'] : null;
$linkAccessRestrictedPages = $arguments['linkAccessRestrictedPages'];
$absolute = $arguments['absolute'];
$addQueryString = $arguments['addQueryString'] ?? false;
$argumentsToBeExcludedFromQueryString = $arguments['argumentsToBeExcludedFromQueryString'];
$uriBuilder = GeneralUtility::makeInstance(ExtbaseUriBuilder::class);
$uri = $uriBuilder
->reset()
->setRequest($request)
->setTargetPageType($pageType)
->setNoCache($noCache)
->setSection($section)
->setLanguage($language)
->setLinkAccessRestrictedPages($linkAccessRestrictedPages)
->setArguments($additionalParams)
->setCreateAbsoluteUri($absolute)
->setAddQueryString($addQueryString)
->setArgumentsToBeExcludedFromQueryString($argumentsToBeExcludedFromQueryString);
if (MathUtility::canBeInterpretedAsInteger($pageUid)) {
$uriBuilder->setTargetPageUid((int)$pageUid);
}
return $uri->build();
}
}
@@ -0,0 +1,176 @@
<?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\Fluid\ViewHelpers\Uri;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolveSystemResourceIdentifierException;
use TYPO3\CMS\Core\SystemResource\Identifier\SystemResourceIdentifierFactory;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\Publishing\UriGenerationOptions;
use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Type\PublicResourceInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\MissingArgumentException;
/**
* ViewHelper for creating URIs to resources (assets).
*
* This ViewHelper should be used to return public url to extension resource files
* for use in html output.
*
* For images within FAL storages, or where graphical operations are
* performed, use `<f:uri.image>` instead.
*
* ```
* <link href="{f:resource(identifier: 'EXT:indexed_search/Resources/Public/Css/Stylesheet.css') -> f:uri.resource()}" rel="stylesheet" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-uri-resource
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-uri-image
*/
final class ResourceViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly SystemResourceFactory $systemResourceFactory,
private readonly SystemResourcePublisherInterface $resourcePublisher,
private readonly SystemResourceIdentifierFactory $resourceIdentifierFactory,
) {}
public function initializeArguments(): void
{
$this->registerArgument('resource', 'object', 'The resource object given as argument or child');
$this->registerArgument('path', 'string', 'The path and filename of the resource (relative to Public resource directory of the extension).');
$this->registerArgument('extensionName', 'string', 'Target extension name. If not set, the current extension name will be used');
$this->registerArgument('absolute', 'bool', 'If set, an absolute URI is rendered', false, false);
$this->registerArgument('useCacheBusting', 'bool', 'If set, the URI is rendered with a cache buster', false, true);
}
/**
* Render the URI to the resource. The filename is used from child content.
*
* @return string The URI to the resource
*/
public function render(): string
{
$resource = $this->renderChildren();
if (!$resource instanceof PublicResourceInterface) {
$resourceIdentifier = $this->resolveResourceIdentifier();
$resource = $this->systemResourceFactory->createPublicResource($resourceIdentifier);
}
$request = $this->resolveRequest();
if ($this->arguments['absolute'] && $request === null) {
throw new \RuntimeException(
'ViewHelper f:uri.resource needs a Request object to generate absolute URLs,',
1758574774
);
}
return (string)$this->resourcePublisher->generateUri(
$resource,
$request,
new UriGenerationOptions(
absoluteUri: $this->arguments['absolute'],
cacheBusting: $this->arguments['useCacheBusting'],
),
);
}
/**
* Explicitly set argument name to be used as content.
*/
public function getContentArgumentName(): string
{
return 'resource';
}
/**
* Resolves the extension path, either directly when possible, or from extension name and request
*/
private function resolveResourceIdentifier(): string
{
if (!isset($this->arguments['path'])) {
throw new MissingArgumentException('ViewHelper f:uri.resource needs either "resource", or "path" argument to be set', 1759231234);
}
$path = $this->arguments['path'];
try {
return (string)$this->resourceIdentifierFactory->create($path);
} catch (CanNotResolveSystemResourceIdentifierException) {
$packageKey = $this->resolveExtensionKey();
$relativePath = 'Resources/Public/' . ltrim($path, '/');
return (string)$this->resourceIdentifierFactory->createFromPackagePath(
$this->resolveExtensionKey(),
'Resources/Public/' . ltrim($path, '/'),
sprintf('Uri\ResourceViewHelper, package key: "%s", relative path: "%s', $packageKey, $relativePath)
);
}
}
/**
* Resolves extension key either from given extension name argument or from request
*/
private function resolveExtensionKey(): string
{
$extensionName = $this->arguments['extensionName'];
if ($extensionName === null) {
return $this->resolveValidatedRequest($this->resolveRequest())->getControllerExtensionKey();
}
return GeneralUtility::camelCaseToLowerCaseUnderscored($extensionName);
}
/**
* Resolves the request from rendering context
*/
private function resolveRequest(): ?ServerRequestInterface
{
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
return $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
return null;
}
/**
* Resolves and validates the request from rendering context
*/
private function resolveValidatedRequest(?ServerRequestInterface $request): RequestInterface
{
if (!$request instanceof RequestInterface) {
throw new \RuntimeException(
sprintf(
'ViewHelper f:uri.resource needs an Extbase Request object to resolve extension name for given path "%s".'
. ' If not in Extbase context, either set argument "extensionName",'
. ' or (better) use the standard EXT: syntax for path attribute like \'path="EXT:indexed_search/Resources/Public/Icons/Extension.svg"\'.',
$this->arguments['path']
),
1639672666
);
}
if ($request->getControllerExtensionKey() === '') {
throw new \RuntimeException(
sprintf(
'Can not resolve extension key for given path "%s".'
. ' If not in Extbase context, either set argument "extensionName",'
. ' or (better) use the standard EXT: syntax for path attribute like \'path="EXT:indexed_search/Resources/Public/Icons/Extension.svg"\'.',
$this->arguments['path']
),
1640097205
);
}
return $request;
}
}
@@ -0,0 +1,116 @@
<?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\Fluid\ViewHelpers\Uri;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
use TYPO3\CMS\Core\LinkHandling\TypolinkParameter;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to create URIs from fields supported by the link wizard.
*
* ```
* <f:uri.typolink parameter="123" textWrap="<span>|</span>" />
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-uri-typolink
*/
final class TypolinkViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly TypoLinkCodecService $typoLinkCodecService
) {}
public function initializeArguments(): void
{
$this->registerArgument('parameter', 'mixed', 'stdWrap.typolink style parameter string', true);
$this->registerArgument('additionalParams', 'string', 'stdWrap.typolink additionalParams', false, '');
$this->registerArgument('language', 'string', 'link to a specific language - defaults to the current language, use a language ID or "current" to enforce a specific language');
$this->registerArgument('addQueryString', 'string', 'If set, the current query parameters will be kept in the URL. If set to "untrusted", then ALL query parameters will be added. Be aware, that this might lead to problems when the generated link is cached.', false, false);
$this->registerArgument('addQueryStringExclude', 'string', 'Define parameters to be excluded from the query string (only active if addQueryString is set)', false, '');
$this->registerArgument('absolute', 'bool', 'Ensure the resulting URL is an absolute URL', false, false);
}
public function render(): string
{
$parameter = $this->arguments['parameter'] ?? '';
if (!$parameter instanceof TypolinkParameter) {
$parameter = TypolinkParameter::createFromTypolinkParts(
is_scalar($parameter) ? $this->typoLinkCodecService->decode((string)$parameter) : []
);
}
// Merge the $parameter with other arguments and encode the typolink again
$typolink = $this->typoLinkCodecService->encode(
TypolinkParameter::createFromTypolinkParts(self::mergeTypoLinkConfiguration($parameter->toArray(), $this->arguments))->toArray()
);
$request = null;
if ($this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
$request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
return $typolink !== '' ? self::invokeContentObjectRenderer($this->arguments, $typolink, $request) : '';
}
private static function invokeContentObjectRenderer(array $arguments, string $typoLinkParameter, ?ServerRequestInterface $request): string
{
$addQueryString = $arguments['addQueryString'] ?? false;
$addQueryStringExclude = $arguments['addQueryStringExclude'] ?? '';
$absolute = $arguments['absolute'] ?? false;
$instructions = [
'parameter' => $typoLinkParameter,
'forceAbsoluteUrl' => $absolute,
];
if (array_key_exists('language', $arguments) && $arguments['language'] !== null) {
$instructions['language'] = (string)$arguments['language'];
}
if ($addQueryString && $addQueryString !== 'false') {
$instructions['addQueryString'] = $addQueryString;
$instructions['addQueryString.'] = [
'exclude' => $addQueryStringExclude,
];
}
$contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
if ($request) {
$contentObject->setRequest($request);
}
return $contentObject->createUrl($instructions);
}
/**
* Merges view helper arguments with typolink parts.
*/
private static function mergeTypoLinkConfiguration(array $typoLinkConfiguration, array $arguments): array
{
if ($typoLinkConfiguration === []) {
return $typoLinkConfiguration;
}
$additionalParameters = $arguments['additionalParams'] ?? '';
// Combine additionalParams
if ($additionalParameters) {
$typoLinkConfiguration['additionalParams'] .= $additionalParameters;
}
return $typoLinkConfiguration;
}
}