first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
commit 1218dd1517
12716 files changed
+989233

No files matched your search

@@ -0,0 +1,196 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form;
/*
* 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!
*/
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Base class for container and single elements - their abstracts extend from here.
*/
abstract class AbstractNode implements NodeInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* Instance of the node factory to create sub elements, container and single element expansions.
*
* @var NodeFactory
*/
protected $nodeFactory;
/**
* Main data array to work on, given from parent to child elements
*
* @var array
*/
protected $data = [];
/**
* A list of default field information added to the element / container.
*
* @var array
*/
protected $defaultFieldInformation = [];
/**
* A list of default field controls added to the element / container.
* This property is often reset by single elements.
*
* @var array
*/
protected $defaultFieldControl = [];
/**
* A list of default field wizards added to the element / container.
* This property is often reset by single elements.
*
* @var array
*/
protected $defaultFieldWizard = [];
/**
* Set data to data array and register node factory to render sub elements
*
* @param NodeFactory $nodeFactory
* @param array $data
*/
public function __construct(NodeFactory $nodeFactory, array $data)
{
$this->data = $data;
$this->nodeFactory = $nodeFactory;
}
/**
* Handler for single nodes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
abstract public function render();
/**
* Initialize the array that is returned to parent after calling. This structure
* is identical for *all* nodes. Parent will merge the return of a child with its
* own stuff and in itself return an array of the same structure.
*
* @return array
*/
protected function initializeResultArray(): array
{
return [
'additionalJavaScriptPost' => [],
'additionalJavaScriptSubmit' => [],
'additionalHiddenFields' => [],
'additionalInlineLanguageLabelFiles' => [],
'stylesheetFiles' => [],
// can hold strings or arrays,
// string = requireJS module,
// array = requireJS module + callback e.g. array('TYPO3/Foo/Bar', 'function() {}')
'requireJsModules' => [],
'inlineData' => [],
'html' => '',
];
}
/**
* Merge existing data with a child return array.
* The incoming $childReturn array should be initialized
* using initializeResultArray() beforehand.
*
* @param array $existing Currently merged array
* @param array $childReturn Array returned by child
* @param bool $mergeHtml If false, the ['html'] section of $childReturn will NOT be added to $existing
* @return array Result array
*/
protected function mergeChildReturnIntoExistingResult(array $existing, array $childReturn, bool $mergeHtml = true): array
{
if ($mergeHtml && !empty($childReturn['html'])) {
$existing['html'] .= LF . $childReturn['html'];
}
foreach ($childReturn['additionalJavaScriptPost'] ?? [] as $value) {
$existing['additionalJavaScriptPost'][] = $value;
}
foreach ($childReturn['additionalJavaScriptSubmit'] ?? [] as $value) {
$existing['additionalJavaScriptSubmit'][] = $value;
}
foreach ($childReturn['additionalHiddenFields'] ?? [] as $value) {
$existing['additionalHiddenFields'][] = $value;
}
foreach ($childReturn['stylesheetFiles'] ?? [] as $value) {
$existing['stylesheetFiles'][] = $value;
}
foreach ($childReturn['requireJsModules'] ?? [] as $module) {
$existing['requireJsModules'][] = $module;
}
foreach ($childReturn['additionalInlineLanguageLabelFiles'] ?? [] as $inlineLanguageLabelFile) {
$existing['additionalInlineLanguageLabelFiles'][] = $inlineLanguageLabelFile;
}
if (!empty($childReturn['inlineData'])) {
$existingInlineData = $existing['inlineData'];
$childInlineData = $childReturn['inlineData'];
ArrayUtility::mergeRecursiveWithOverrule($existingInlineData, $childInlineData);
$existing['inlineData'] = $existingInlineData;
}
return $existing;
}
/**
* Build JSON string for validations rules.
*
* @param array $config
* @return string
*/
protected function getValidationDataAsJsonString(array $config): string
{
$validationRules = [];
if (!empty($config['eval'])) {
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
foreach ($evalList as $evalType) {
$validationRules[] = [
'type' => $evalType,
];
}
}
if (!empty($config['range'])) {
$newValidationRule = [
'type' => 'range',
];
if (!empty($config['range']['lower'])) {
$newValidationRule['lower'] = $config['range']['lower'];
}
if (!empty($config['range']['upper'])) {
$newValidationRule['upper'] = $config['range']['upper'];
}
$validationRules[] = $newValidationRule;
}
if (!empty($config['maxitems']) || !empty($config['minitems'])) {
$minItems = isset($config['minitems']) ? (int)$config['minitems'] : 0;
$maxItems = isset($config['maxitems']) ? (int)$config['maxitems'] : 99999;
$type = $config['type'] ?: 'range';
$validationRules[] = [
'type' => $type,
'minItems' => $minItems,
'maxItems' => $maxItems
];
}
if (!empty($config['required'])) {
$validationRules[] = ['type' => 'required'];
}
return json_encode($validationRules);
}
}
@@ -0,0 +1,128 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* Abstract container has various methods used by the container classes
*/
abstract class AbstractContainer extends AbstractNode
{
/**
* Merge field information configuration with default and render them.
*
* @return array Result array
*/
protected function renderFieldInformation(): array
{
$options = $this->data;
$fieldInformation = $this->defaultFieldInformation;
$currentRenderType = $this->data['renderType'];
$fieldInformationFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldInformation'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldInformation, $fieldInformationFromTca);
$options['renderType'] = 'fieldInformation';
$options['renderData']['fieldInformation'] = $fieldInformation;
return $this->nodeFactory->create($options)->render();
}
/**
* Merge field control configuration with default controls and render them.
*
* @return array Result array
*/
protected function renderFieldControl(): array
{
$options = $this->data;
$fieldControl = $this->defaultFieldControl;
$currentRenderType = $this->data['renderType'];
$fieldControlFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldControl'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca);
$options['renderType'] = 'fieldControl';
$options['renderData']['fieldControl'] = $fieldControl;
return $this->nodeFactory->create($options)->render();
}
/**
* Merge field wizard configuration with default wizards and render them.
*
* @return array Result array
*/
protected function renderFieldWizard(): array
{
$options = $this->data;
$fieldWizard = $this->defaultFieldWizard;
$currentRenderType = $this->data['renderType'];
$fieldWizardFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldWizard'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldWizard, $fieldWizardFromTca);
$options['renderType'] = 'fieldWizard';
$options['renderData']['fieldWizard'] = $fieldWizard;
return $this->nodeFactory->create($options)->render();
}
/**
* A single field of TCA 'types' 'showitem' can have three semicolon separated configuration options:
* fieldName: Name of the field to be found in TCA 'columns' section
* fieldLabel: An alternative field label
* paletteName: Name of a palette to be found in TCA 'palettes' section that is rendered after this field
*
* @param string $field Semicolon separated field configuration
* @throws \RuntimeException
* @return array
*/
protected function explodeSingleFieldShowItemConfiguration($field)
{
$fieldArray = GeneralUtility::trimExplode(';', $field);
if (empty($fieldArray[0])) {
throw new \RuntimeException('Field must not be empty', 1426448465);
}
return [
'fieldName' => $fieldArray[0],
'fieldLabel' => !empty($fieldArray[1]) ? $fieldArray[1] : null,
'paletteName' => !empty($fieldArray[2]) ? $fieldArray[2] : null,
];
}
/**
* Render tabs with label and content. Used by TabsContainer and FlexFormTabsContainer.
* Re-uses the template Tabs.html which is also used by ModuleTemplate.php.
*
* @param array $menuItems Tab elements, each element is an array with "label" and "content"
* @param string $domId DOM id attribute, will be appended with an iteration number per tab.
* @param int $defaultTabIndex
* @return string
*/
protected function renderTabMenu(array $menuItems, $domId, $defaultTabIndex = 1)
{
$templatePath = ExtensionManagementUtility::extPath('backend')
. 'Resources/Private/Templates/DocumentTemplate/';
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setTemplatePathAndFilename($templatePath . 'Tabs.html');
$view->setPartialRootPaths([$templatePath . 'Partials']);
$view->assignMultiple([
'id' => $domId,
'items' => $menuItems,
'defaultTabIndex' => $defaultTabIndex,
'wrapContent' => false,
'storeLastActiveTab' => true,
]);
return $view->render();
}
}
@@ -0,0 +1,139 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Flex form container implementation
* This one is called by FlexFormSectionContainer and renders HTML for a single container.
* For processing of single elements FlexFormElementContainer is called
*/
class FlexFormContainerContainer extends AbstractContainer
{
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$table = $this->data['tableName'];
$row = $this->data['databaseRow'];
$fieldName = $this->data['fieldName'];
$flexFormFormPrefix = $this->data['flexFormFormPrefix'];
$flexFormContainerElementCollapsed = $this->data['flexFormContainerElementCollapsed'];
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
$parameterArray = $this->data['parameterArray'];
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$toggleIcons = '<span class="t3js-flex-control-toggle-icon-open" style="' . ($flexFormContainerElementCollapsed ? 'display: none;' : '') . '">'
. $iconFactory->getIcon('actions-view-list-collapse', Icon::SIZE_SMALL)->render()
. '</span>';
$toggleIcons .= '<span class="t3js-flex-control-toggle-icon-close" style="' . ($flexFormContainerElementCollapsed ? '' : 'display: none;') . '">'
. $iconFactory->getIcon('actions-view-list-expand', Icon::SIZE_SMALL)->render()
. '</span>';
$flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier'];
$actionFieldName = '_ACTION_FLEX_FORM'
. $parameterArray['itemFormElName']
. $this->data['flexFormFormPrefix']
. '[_ACTION]'
. '[' . $flexFormContainerIdentifier . ']';
$toggleFieldName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'
. $flexFormFormPrefix
. '[' . $flexFormContainerIdentifier . ']'
. '[_TOGGLE]';
$moveAndDeleteContent = [];
$userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
if ($userHasAccessToDefaultLanguage) {
$moveAndDeleteContent[] = '<span class="btn btn-default t3js-sortable-handle"><span title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:sortable.dragmove')) . '">' . $iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '</span></span>';
$moveAndDeleteContent[] = '<span class="btn btn-default t3js-delete"><span title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:delete')) . '">' . $iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</span></span>';
}
$options = $this->data;
// Append container specific stuff to field prefix
$options['flexFormFormPrefix'] = $flexFormFormPrefix . '[' . $flexFormContainerIdentifier . '][' . $this->data['flexFormContainerName'] . '][el]';
$options['flexFormDataStructureArray'] = $flexFormDataStructureArray['el'];
$options['renderType'] = 'flexFormElementContainer';
$containerContentResult = $this->nodeFactory->create($options)->render();
$containerTitle = '';
if (!empty(trim($flexFormDataStructureArray['title']))) {
$containerTitle = $languageService->sL(trim($flexFormDataStructureArray['title']));
}
$resultArray = $this->initializeResultArray();
$html = [];
$html[] = '<div class="t3-form-field-container-flexsections t3-flex-section t3js-flex-section">';
$html[] = '<input class="t3-flex-control t3js-flex-control-action" type="hidden" name="' . htmlspecialchars($actionFieldName) . '" value="" />';
$html[] = '<div class="panel panel-default panel-condensed">';
$html[] = '<div class="panel-heading t3js-flex-section-header" data-toggle="formengine-flex">';
$html[] = '<div class="form-irre-header">';
$html[] = '<div class="form-irre-header-cell form-irre-header-icon">';
$html[] = $toggleIcons;
$html[] = '</div>';
$html[] = '<div class="form-irre-header-cell form-irre-header-body">';
$html[] = '<span class="t3js-record-title">' . htmlspecialchars($containerTitle) . '</span>';
$html[] = '</div>';
$html[] = '<div class="form-irre-header-cell form-irre-header-control">';
$html[] = '<div class="btn-group btn-group-sm">';
$html[] = implode(LF, $moveAndDeleteContent);
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<div class="panel-collapse t3js-flex-section-content"' . ($flexFormContainerElementCollapsed ? ' style="display:none;"' : '') . '>';
$html[] = $containerContentResult['html'];
$html[] = '</div>';
$html[] = '<input';
$html[] = 'class="t3-flex-control t3js-flex-control-toggle"';
$html[] = 'type="hidden"';
$html[] = 'name="' . htmlspecialchars($toggleFieldName) . '"';
$html[] = 'value="' . ($flexFormContainerElementCollapsed ? '1' : '0') . '"';
$html[] = '/>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $containerContentResult, false);
return $resultArray;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,179 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
/**
* The container handles single elements.
*
* This one is called by FlexFormTabsContainer, FlexFormNoTabsContainer or FlexFormContainerContainer.
* For single fields, the code is similar to SingleFieldContainer, processing will end up in single
* element classes depending on specific renderType of an element. Additionally, it determines if a
* section is handled and hands over to FlexFormSectionContainer in this case.
*/
class FlexFormElementContainer extends AbstractContainer
{
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
$flexFormRowData = $this->data['flexFormRowData'];
$flexFormFormPrefix = $this->data['flexFormFormPrefix'];
$parameterArray = $this->data['parameterArray'];
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
foreach ($flexFormDataStructureArray as $flexFormFieldName => $flexFormFieldArray) {
if (
// No item array found at all
!is_array($flexFormFieldArray)
// Not a section or container and not a list of single items
|| (!isset($flexFormFieldArray['type']) && !is_array($flexFormFieldArray['config']))
) {
continue;
}
if ($flexFormFieldArray['type'] === 'array') {
// Section
if (empty($flexFormFieldArray['section'])) {
$resultArray['html'] = LF . 'Section expected at ' . $flexFormFieldName . ' but not found';
continue;
}
$options = $this->data;
$options['flexFormDataStructureArray'] = $flexFormFieldArray;
$options['flexFormRowData'] = $flexFormRowData[$flexFormFieldName]['el'] ?? [];
$options['flexFormFieldName'] = $flexFormFieldName;
$options['renderType'] = 'flexFormSectionContainer';
$sectionContainerResult = $this->nodeFactory->create($options)->render();
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $sectionContainerResult);
} else {
// Set up options for single element
$fakeParameterArray = [
'fieldConf' => [
'label' => $languageService->sL(trim($flexFormFieldArray['label'])),
'config' => $flexFormFieldArray['config'],
'children' => $flexFormFieldArray['children'],
'onChange' => $flexFormFieldArray['onChange'],
],
'fieldChangeFunc' => $parameterArray['fieldChangeFunc'],
'label' => $parameterArray['label'],
];
if (isset($flexFormFieldArray['description']) && !empty($flexFormFieldArray['description'])) {
$fakeParameterArray['fieldConf']['description'] = $flexFormFieldArray['description'];
}
$alertMsgOnChange = '';
if (isset($fakeParameterArray['fieldConf']['onChange']) && $fakeParameterArray['fieldConf']['onChange'] === 'reload') {
if ($this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
$alertMsgOnChange = 'top.TYPO3.Modal.confirm('
. 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
. ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
. ')'
. '.on('
. '"button.clicked",'
. ' function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); }'
. ');';
} else {
$alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm();}';
}
}
if ($alertMsgOnChange) {
$fakeParameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
}
$originalFieldName = $parameterArray['itemFormElName'];
$fakeParameterArray['itemFormElName'] = $parameterArray['itemFormElName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]';
if ($fakeParameterArray['itemFormElName'] !== $originalFieldName) {
// If calculated itemFormElName is different from originalFieldName
// change the originalFieldName in TBE_EDITOR_fieldChanged. This is
// especially relevant for wizards writing their content back to hidden fields
if (!empty($fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'])) {
$fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = str_replace($originalFieldName, $fakeParameterArray['itemFormElName'], $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']);
}
}
$fakeParameterArray['itemFormElID'] = $parameterArray['itemFormElID'] . '_' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $flexFormFieldName) . '_' . md5($fakeParameterArray['itemFormElName']);
if (isset($flexFormRowData[$flexFormFieldName]['vDEF'])) {
$fakeParameterArray['itemFormElValue'] = $flexFormRowData[$flexFormFieldName]['vDEF'];
} else {
$fakeParameterArray['itemFormElValue'] = $fakeParameterArray['fieldConf']['config']['default'];
}
$options = $this->data;
// Set either flexFormFieldName or flexFormContainerFieldName, depending on if we are a "regular" field or a flex container section field
if (empty($options['flexFormFieldName'])) {
$options['flexFormFieldName'] = $flexFormFieldName;
} else {
$options['flexFormContainerFieldName'] = $flexFormFieldName;
}
$options['parameterArray'] = $fakeParameterArray;
$options['elementBaseName'] = $this->data['elementBaseName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]';
if (!empty($flexFormFieldArray['config']['renderType'])) {
$options['renderType'] = $flexFormFieldArray['config']['renderType'];
} else {
// Fallback to type if no renderType is given
$options['renderType'] = $flexFormFieldArray['config']['type'];
}
$childResult = $this->nodeFactory->create($options)->render();
if (!empty($childResult['html'])) {
// Possible line breaks in the label through xml: \n => <br/>, usage of nl2br() not possible, so it's done through str_replace (?!)
$processedTitle = str_replace('\\n', '<br />', htmlspecialchars($fakeParameterArray['fieldConf']['label']));
$html = [];
$html[] = '<div class="form-section">';
$html[] = '<div class="form-group t3js-formengine-palette-field t3js-formengine-validation-marker">';
$html[] = '<label class="t3js-formengine-label">';
$html[] = BackendUtility::wrapInHelp($parameterArray['_cshKey'], $flexFormFieldName, $processedTitle);
$html[] = '</label>';
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $childResult['html'];
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] .= implode(LF, $html);
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
}
}
}
return $resultArray;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,51 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
/**
* Entry container to a flex form element. This container is created by
* SingleFieldContainer if a type='flex' field is rendered.
*
* It either forks a FlexFormTabsContainer or a FlexFormNoTabsContainer.
*/
class FlexFormEntryContainer extends AbstractContainer
{
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$flexFormDataStructureIdentifier = $this->data['parameterArray']['fieldConf']['config']['dataStructureIdentifier'];
$flexFormDataStructureArray = $this->data['parameterArray']['fieldConf']['config']['ds'];
$options = $this->data;
$options['flexFormDataStructureIdentifier'] = $flexFormDataStructureIdentifier;
$options['flexFormDataStructureArray'] = $flexFormDataStructureArray;
$options['flexFormRowData'] = $this->data['parameterArray']['itemFormElValue'];
$options['renderType'] = 'flexFormNoTabsContainer';
// Enable tabs if there is more than one sheet
if (count($flexFormDataStructureArray['sheets']) > 1) {
$options['renderType'] = 'flexFormTabsContainer';
}
$resultArray = $this->nodeFactory->create($options)->render();
$resultArray['requireJsModules'][] = 'TYPO3/CMS/Backend/FormEngineFlexForm';
return $resultArray;
}
}
@@ -0,0 +1,95 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Handle a flex form that has no tabs.
*
* This container is called by FlexFormEntryContainer if only a default sheet
* exists. It evaluates the display condition and hands over rendering of single
* fields to FlexFormElementContainer.
*/
class FlexFormNoTabsContainer extends AbstractContainer
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$table = $this->data['tableName'];
$row = $this->data['databaseRow'];
$fieldName = $this->data['fieldName']; // field name of the flex form field in DB
$parameterArray = $this->data['parameterArray'];
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
$flexFormRowData = $this->data['flexFormRowData'];
$resultArray = $this->initializeResultArray();
// Determine this single sheet name, most often it ends up with sDEF, except if only one sheet was defined
$flexFormSheetNames = array_keys($flexFormDataStructureArray['sheets']);
$sheetName = array_pop($flexFormSheetNames);
$flexFormRowDataSubPart = $flexFormRowData['data'][$sheetName]['lDEF'] ?: [];
unset($flexFormDataStructureArray['meta']);
if (!is_array($flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el'])) {
$resultArray['html'] = 'Data Structure ERROR: No [\'ROOT\'][\'el\'] element found in flex form definition.';
return $resultArray;
}
// Assemble key for loading the correct CSH file
// @todo: what is that good for? That is for the title of single elements ... see FlexFormElementContainer!
$dsPointerFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['ds_pointerField'], true);
$parameterArray['_cshKey'] = $table . '.' . $fieldName;
foreach ($dsPointerFields as $key) {
if (is_string($row[$key]) && $row[$key] !== '') {
$parameterArray['_cshKey'] .= '.' . $row[$key];
} elseif (is_array($row[$key]) && isset($row[$key][0]) && is_string($row[$key][0]) && $row[$key][0] !== '') {
$parameterArray['_cshKey'] .= '.' . $row[$key][0];
}
}
$options = $this->data;
$options['flexFormDataStructureArray'] = $flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el'];
$options['flexFormRowData'] = $flexFormRowDataSubPart;
$options['flexFormSheetName'] = $sheetName;
$options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]';
$options['parameterArray'] = $parameterArray;
$resultArray = $this->initializeResultArray();
$fieldInformationResult = $this->renderFieldInformation();
$resultArray['html'] = '<div>' . $fieldInformationResult['html'] . '</div>';
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$options['renderType'] = 'flexFormElementContainer';
$childResult = $this->nodeFactory->create($options)->render();
return $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, true);
}
}
@@ -0,0 +1,162 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Handle flex form sections.
*
* This container is created by FlexFormElementContainer if a "single" element is in
* fact a section. For each existing section container it creates as FlexFormContainerContainer
* to render its inner fields.
* Additionally, a button for each possible container is rendered with information for the
* ajax controller that fetches one on click.
*/
class FlexFormSectionContainer extends AbstractContainer
{
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$languageService = $this->getLanguageService();
$flexFormDataStructureIdentifier = $this->data['flexFormDataStructureIdentifier'];
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
$flexFormRowData = $this->data['flexFormRowData'];
$flexFormFieldName = $this->data['flexFormFieldName'];
$flexFormSheetName = $this->data['flexFormSheetName'];
$userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
$resultArray = $this->initializeResultArray();
// Render each existing container
foreach ($flexFormDataStructureArray['children'] as $flexFormContainerIdentifier => $containerDataStructure) {
$existingContainerData = $flexFormRowData[$flexFormContainerIdentifier];
// @todo: This relies on the fact that "_TOGGLE" is *below* the real data in the saved xml structure
$existingSectionContainerDataStructureType = key($existingContainerData);
$existingContainerData = $existingContainerData[$existingSectionContainerDataStructureType];
$options = $this->data;
$options['flexFormRowData'] = $existingContainerData['el'];
$options['flexFormDataStructureArray'] = $containerDataStructure;
$options['flexFormFormPrefix'] = $this->data['flexFormFormPrefix'] . '[' . $flexFormFieldName . ']' . '[el]';
$options['flexFormContainerName'] = $existingSectionContainerDataStructureType;
$options['flexFormContainerIdentifier'] = $flexFormContainerIdentifier;
$options['flexFormContainerElementCollapsed'] = (bool)$flexFormRowData[$flexFormContainerIdentifier]['_TOGGLE'];
$options['renderType'] = 'flexFormContainerContainer';
$flexFormContainerContainerResult = $this->nodeFactory->create($options)->render();
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormContainerContainerResult);
}
// "New container" handling: Creates buttons for each possible container with all relevant information for the ajax call.
$containerTemplatesHtml = [];
foreach ($flexFormDataStructureArray['el'] as $flexFormContainerName => $flexFormFieldDefinition) {
$containerTitle = '';
if (!empty(trim($flexFormFieldDefinition['title']))) {
$containerTitle = $languageService->sL(trim($flexFormFieldDefinition['title']));
}
$containerTemplateHtml = [];
$containerTemplateHtml[] = '<a';
$containerTemplateHtml[] = 'href="#"';
$containerTemplateHtml[] = 'class="btn btn-default t3js-flex-container-add"';
$containerTemplateHtml[] = 'data-vanillauid="' . (int)$this->data['vanillaUid'] . '"';
// no int cast for databaseRow uid, this can be "NEW1234..."
$containerTemplateHtml[] = 'data-databaserowuid="' . htmlspecialchars($this->data['databaseRow']['uid']) . '"';
$containerTemplateHtml[] = 'data-command="' . htmlspecialchars($this->data['command']) . '"';
$containerTemplateHtml[] = 'data-tablename="' . htmlspecialchars($this->data['tableName']) . '"';
$containerTemplateHtml[] = 'data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"';
$containerTemplateHtml[] = 'data-recordtypevalue="' . $this->data['recordTypeValue'] . '"';
$containerTemplateHtml[] = 'data-datastructureidentifier="' . htmlspecialchars($flexFormDataStructureIdentifier) . '"';
$containerTemplateHtml[] = 'data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
$containerTemplateHtml[] = 'data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
$containerTemplateHtml[] = 'data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
$containerTemplateHtml[] = '>';
$containerTemplateHtml[] = $iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render();
$containerTemplateHtml[] = htmlspecialchars(GeneralUtility::fixed_lgd_cs($containerTitle, 30));
$containerTemplateHtml[] = '</a>';
$containerTemplatesHtml[] = implode(LF, $containerTemplateHtml);
}
// Create new elements links
$createElementsHtml = [];
if ($userHasAccessToDefaultLanguage) {
$createElementsHtml[] = '<div class="t3-form-field-add-flexsection">';
$createElementsHtml[] = '<div class="btn-group">';
$createElementsHtml[] = implode('', $containerTemplatesHtml);
$createElementsHtml[] = '</div>';
$createElementsHtml[] = '</div>';
}
$sectionTitle = '';
if (!empty(trim($flexFormDataStructureArray['title']))) {
$sectionTitle = $languageService->sL(trim($flexFormDataStructureArray['title']));
}
// Wrap child stuff
$toggleAll = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall'));
$html = [];
$html[] = '<div class="panel panel-tab">';
$html[] = '<div class="panel-body">';
$html[] = '<div class="t3-form-field-container t3-form-flex">';
$html[] = '<div class="t3-form-field-label-flexsection">';
$html[] = '<h4>';
$html[] = htmlspecialchars($sectionTitle);
$html[] = '</h4>';
$html[] = '</div>';
$html[] = '<div class="t3js-form-field-toggle-flexsection t3-form-flexsection-toggle">';
$html[] = '<a class="btn btn-default" href="#" title="' . $toggleAll . '">';
$html[] = $iconFactory->getIcon('actions-move-right', Icon::SIZE_SMALL)->render() . $toggleAll;
$html[] = '</a>';
$html[] = '</div>';
$html[] = '<div';
$html[] = 'class="panel-group panel-hover t3-form-field-container-flexsection t3-flex-container"';
$html[] = 'data-t3-flex-allow-restructure="' . ($userHasAccessToDefaultLanguage ? '1' : '0') . '"';
$html[] = '>';
$html[] = $resultArray['html'];
$html[] = '</div>';
$html[] = implode(LF, $createElementsHtml);
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,125 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Handle flex forms that have tabs (multiple "sheets").
*
* This container is called by FlexFormEntryContainer. It resolves each
* sheet and hands rendering of single sheet content over to FlexFormElementContainer.
*/
class FlexFormTabsContainer extends AbstractContainer
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$table = $this->data['tableName'];
$row = $this->data['databaseRow'];
$fieldName = $this->data['fieldName']; // field name of the flex form field in DB
$parameterArray = $this->data['parameterArray'];
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
$flexFormRowData = $this->data['flexFormRowData'];
$resultArray = $this->initializeResultArray();
$resultArray['requireJsModules'][] = 'TYPO3/CMS/Backend/Tabs';
$domIdPrefix = 'DTM-' . GeneralUtility::shortMD5($this->data['parameterArray']['itemFormElName']);
$tabCounter = 0;
$tabElements = [];
foreach ($flexFormDataStructureArray['sheets'] as $sheetName => $sheetDataStructure) {
$flexFormRowSheetDataSubPart = $flexFormRowData['data'][$sheetName]['lDEF'] ?: [];
if (!is_array($sheetDataStructure['ROOT']['el'])) {
$resultArray['html'] .= LF . 'No Data Structure ERROR: No [\'ROOT\'][\'el\'] found for sheet "' . $sheetName . '".';
continue;
}
$tabCounter++;
// Assemble key for loading the correct CSH file
// @todo: what is that good for? That is for the title of single elements ... see FlexFormElementContainer!
$dsPointerFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['ds_pointerField'], true);
$parameterArray['_cshKey'] = $table . '.' . $fieldName;
foreach ($dsPointerFields as $key) {
if (is_string($row[$key]) && $row[$key] !== '') {
$parameterArray['_cshKey'] .= '.' . $row[$key];
} elseif (is_array($row[$key]) && isset($row[$key][0]) && is_string($row[$key][0]) && $row[$key][0] !== '') {
$parameterArray['_cshKey'] .= '.' . $row[$key][0];
}
}
$options = $this->data;
$options['flexFormDataStructureArray'] = $sheetDataStructure['ROOT']['el'];
$options['flexFormRowData'] = $flexFormRowSheetDataSubPart;
$options['flexFormSheetName'] = $sheetName;
$options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]';
$options['parameterArray'] = $parameterArray;
// Merge elements of this tab into a single list again and hand over to
// palette and single field container to render this group
$options['tabAndInlineStack'][] = [
'tab',
$domIdPrefix . '-' . $tabCounter,
];
$options['renderType'] = 'flexFormElementContainer';
$childReturn = $this->nodeFactory->create($options)->render();
if ($childReturn['html'] !== '') {
$tabElements[] = [
'label' => !empty(trim($sheetDataStructure['ROOT']['sheetTitle'])) ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetTitle'])) : $sheetName,
'content' => $childReturn['html'],
'description' => trim($sheetDataStructure['ROOT']['sheetDescription']) ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetDescription'])) : '',
'linkTitle' => trim($sheetDataStructure['ROOT']['sheetShortDescr']) ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetShortDescr'])) : '',
];
}
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childReturn, false);
}
$fieldInformationResult = $this->renderFieldInformation();
$resultArray['html'] = '<div>' . $fieldInformationResult['html'] . '</div>';
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$resultArray['html'] .= $this->renderTabMenu($tabElements, $domIdPrefix);
return $resultArray;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,93 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* A container rendering a "full record". This is an entry container used as first
* step into the rendering tree..
*
* This container determines the to be rendered fields depending on the record type,
* initializes possible language base data, finds out if tabs should be rendered and
* then calls either TabsContainer or a NoTabsContainer for further processing.
*/
class FullRecordContainer extends AbstractContainer
{
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$table = $this->data['tableName'];
$recordTypeValue = $this->data['recordTypeValue'];
// Load the description content for the table if requested
if (!empty($this->data['processedTca']['interface']['always_description'])) {
$languageService = $this->getLanguageService();
$languageService->loadSingleTableDescription($table);
}
// List of items to be rendered
$itemList = $this->data['processedTca']['types'][$recordTypeValue]['showitem'];
$fieldsArray = GeneralUtility::trimExplode(',', $itemList, true);
// Streamline the fields array
// First, make sure there is always a --div-- definition for the first element
if (strpos($fieldsArray[0], '--div--') !== 0) {
array_unshift($fieldsArray, '--div--;LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.generalTab');
}
// If first tab has no label definition, add "general" label
$firstTabHasLabel = count(GeneralUtility::trimExplode(';', $fieldsArray[0])) > 1;
if (!$firstTabHasLabel) {
$fieldsArray[0] = '--div--;LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.generalTab';
}
// If there are at least two --div-- definitions, inner container will be a TabContainer, else a NoTabContainer
$tabCount = 0;
foreach ($fieldsArray as $field) {
if (strpos($field, '--div--') === 0) {
$tabCount++;
}
}
$hasTabs = true;
if ($tabCount < 2) {
// Remove first tab definition again if there is only one tab defined
array_shift($fieldsArray);
$hasTabs = false;
}
$data = $this->data;
$data['fieldsArray'] = $fieldsArray;
if ($hasTabs) {
$data['renderType'] = 'tabsContainer';
} else {
$data['renderType'] = 'noTabsContainer';
}
return $this->nodeFactory->create($data)->render();
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,683 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Inline element entry container.
*
* This container is the entry step to rendering an inline element. It is created by SingleFieldContainer.
*
* The code creates the main structure for the single inline elements, initializes
* the inlineData array, that is manipulated and also returned back in its manipulated state.
* The "control" stuff of inline elements is rendered here, for example the "create new" button.
*
* For each existing inline relation an InlineRecordContainer is called for further processing.
*/
class InlineControlContainer extends AbstractContainer
{
/**
* Inline data array used in JS, returned as JSON object to frontend
*
* @var array
*/
protected $inlineData = [];
/**
* @var InlineStackProcessor
*/
protected $inlineStackProcessor;
/**
* @var IconFactory
*/
protected $iconFactory;
/**
* @var string[]
*/
protected $requireJsModules = [];
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* @var array Default wizards
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
];
/**
* Container objects give $nodeFactory down to other containers.
*
* @param NodeFactory $nodeFactory
* @param array $data
*/
public function __construct(NodeFactory $nodeFactory, array $data)
{
parent::__construct($nodeFactory, $data);
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
}
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$this->inlineData = $this->data['inlineData'];
/** @var InlineStackProcessor $inlineStackProcessor */
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$this->inlineStackProcessor = $inlineStackProcessor;
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
$table = $this->data['tableName'];
$row = $this->data['databaseRow'];
$field = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
$foreign_table = $config['foreign_table'];
$isReadOnly = isset($config['readOnly']) && $config['readOnly'];
$language = 0;
$languageFieldName = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
if (BackendUtility::isTableLocalizable($table)) {
$language = isset($row[$languageFieldName][0]) ? (int)$row[$languageFieldName][0] : (int)$row[$languageFieldName];
}
// Add the current inline job to the structure stack
$newStructureItem = [
'table' => $table,
'uid' => $row['uid'],
'field' => $field,
'config' => $config,
];
// Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
if (!empty($parameterArray['itemFormElName'])) {
$flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']);
if ($flexFormParts !== null) {
$newStructureItem['flexform'] = $flexFormParts;
}
}
$inlineStackProcessor->pushStableStructureItem($newStructureItem);
// Transport the flexform DS identifier fields to the FormInlineAjaxController
if (!empty($newStructureItem['flexform'])
&& isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'])
) {
$config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'];
}
// Hand over original returnUrl to FormInlineAjaxController. Needed if opening for instance a
// nested element in a new view to then go back to the original returnUrl and not the url of
// the inline ajax controller
$config['originalReturnUrl'] = $this->data['returnUrl'];
// e.g. data[<table>][<uid>][<field>]
$nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix();
// e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
$nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
$config['inline']['first'] = false;
$firstChild = reset($this->data['parameterArray']['fieldConf']['children']);
if (isset($firstChild['databaseRow']['uid'])) {
$config['inline']['first'] = $firstChild['databaseRow']['uid'];
}
$config['inline']['last'] = false;
$lastChild = end($this->data['parameterArray']['fieldConf']['children']);
if (isset($lastChild['databaseRow']['uid'])) {
$config['inline']['last'] = $lastChild['databaseRow']['uid'];
}
$top = $inlineStackProcessor->getStructureLevel(0);
$this->inlineData['config'][$nameObject] = [
'table' => $foreign_table,
'md5' => md5($nameObject)
];
$configJson = json_encode($config);
$this->inlineData['config'][$nameObject . '-' . $foreign_table] = [
'min' => $config['minitems'],
'max' => $config['maxitems'],
'sortable' => $config['appearance']['useSortable'],
'top' => [
'table' => $top['table'],
'uid' => $top['uid']
],
'context' => [
'config' => $configJson,
'hmac' => GeneralUtility::hmac($configJson, 'InlineContext'),
],
];
$this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
$uniqueMax = 0;
$uniqueIds = [];
if ($config['foreign_unique']) {
// Add inlineData['unique'] with JS unique configuration
// @todo: Improve validation and throw an exception if type is neither select nor group here
$type = $config['selectorOrUniqueConfiguration']['config']['type'] === 'select' ? 'select' : 'groupdb';
foreach ($parameterArray['fieldConf']['children'] as $child) {
// Determine used unique ids, skip not localized records
if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$value = $child['databaseRow'][$config['foreign_unique']];
// We're assuming there is only one connected value here for both select and group
if ($type === 'select') {
// A select field is an array of uids. See TcaSelectItems data provider for details.
// Pick first entry, ends up as eg. $value = 42.
$value = $value['0'];
} else {
// A group field is an array of arrays containing uid + table + title + row.
// See TcaGroup data provider for details.
// Pick the first one (always on 0), and use uid + table only. Exclude title + row
// since the entire inlineData['unique'] array ends up in JavaScript in the end
// and we don't need and want the title and the entire row data in the frontend.
// Ends up as $value = [ 'uid' => '42', 'table' => 'tx_my_table' ]
$value = [
'uid' => $value[0]['uid'],
'table' => $value[0]['table'],
];
}
// Note structure of $value is different in select vs. group: It's a uid for select, but an
// array with uid + table for group. This is handled differently on JavaScript side, search
// for 'groupdb' in jsfunc.inline.js for details.
$uniqueIds[$child['databaseRow']['uid']] = $value;
}
}
$possibleRecords = $config['selectorOrUniquePossibleRecords'];
$possibleRecordsUidToTitle = [];
foreach ($possibleRecords as $possibleRecord) {
$possibleRecordsUidToTitle[$possibleRecord[1]] = $possibleRecord[0];
}
$uniqueMax = $config['appearance']['useCombination'] || empty($possibleRecords) ? -1 : count($possibleRecords);
$this->inlineData['unique'][$nameObject . '-' . $foreign_table] = [
'max' => $uniqueMax,
'used' => $uniqueIds,
'type' => $type,
'table' => $foreign_table,
'elTable' => $config['selectorOrUniqueConfiguration']['foreignTable'],
'field' => $config['foreign_unique'],
'selector' => $config['selectorOrUniqueConfiguration']['isSelector'] ? $type : false,
'possible' => $possibleRecordsUidToTitle,
];
}
$resultArray['inlineData'] = $this->inlineData;
// @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider
$uidOfDefaultRecord = $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
$isLocalizedParent = $language > 0
&& ($uidOfDefaultRecord[0] ?? $uidOfDefaultRecord) > 0
&& MathUtility::canBeInterpretedAsInteger($row['uid']);
$numberOfFullLocalizedChildren = 0;
$numberOfNotYetLocalizedChildren = 0;
foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$numberOfFullLocalizedChildren++;
}
if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$numberOfNotYetLocalizedChildren++;
}
}
// Render the localization links if needed
$localizationLinks = '';
if ($numberOfNotYetLocalizedChildren) {
// Add the "Localize all records" link before all child records:
if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) {
$localizationLinks = ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config);
}
// Add the "Synchronize with default language" link before all child records:
if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) {
$localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config);
}
}
// Define how to show the "Create new record" link - if there are more than maxitems, hide it
if ($isReadOnly || $numberOfFullLocalizedChildren >= $config['maxitems'] || ($uniqueMax > 0 && $numberOfFullLocalizedChildren >= $uniqueMax)) {
$config['inline']['inlineNewButtonStyle'] = 'display: none;';
$config['inline']['inlineNewRelationButtonStyle'] = 'display: none;';
$config['inline']['inlineOnlineMediaAddButtonStyle'] = 'display: none;';
}
// Render the level links (create new record):
$levelLinks = '';
if (!empty($config['appearance']['enabledControls']['new'])) {
$levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config);
}
// Wrap all inline fields of a record with a <div> (like a container)
$html = '<div class="form-group" id="' . $nameObject . '">';
$fieldInformationResult = $this->renderFieldInformation();
$html .= $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
// Add the level links before all child records:
if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
$html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>';
}
// If it's required to select from possible child records (reusable children), add a selector box
if (!$isReadOnly && $config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== false) {
if ($config['selectorOrUniqueConfiguration']['config']['type'] === 'select') {
$selectorBox = $this->renderPossibleRecordsSelectorTypeSelect($config, $uniqueIds);
} else {
$selectorBox = $this->renderPossibleRecordsSelectorTypeGroupDB($config);
}
$html .= $selectorBox . $localizationLinks;
}
$title = $languageService->sL(trim($parameterArray['fieldConf']['label']));
$html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">';
$sortableRecordUids = [];
foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) {
$options['inlineParentUid'] = $row['uid'];
$options['inlineFirstPid'] = $this->data['inlineFirstPid'];
// @todo: this can be removed if this container no longer sets additional info to $config
$options['inlineParentConfig'] = $config;
$options['inlineData'] = $this->inlineData;
$options['inlineStructure'] = $inlineStackProcessor->getStructure();
$options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
$options['renderType'] = 'inlineRecordContainer';
$childResult = $this->nodeFactory->create($options)->render();
$html .= $childResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
// Don't add record to list of "valid" uids if it is only the default
// language record of a not yet localized child
$sortableRecordUids[] = $options['databaseRow']['uid'];
}
}
$html .= '</div>';
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html .= $fieldWizardHtml;
// Add the level links after all child records:
if (!$isReadOnly && ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom')) {
$html .= $levelLinks . $localizationLinks;
}
if (is_array($config['customControls'])) {
$html .= '<div id="' . $nameObject . '_customControls">';
foreach ($config['customControls'] as $customControlConfig) {
if (!isset($customControlConfig['userFunc'])) {
trigger_error('Support for customControl without a userFunc key will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
$customControlConfig = [
'userFunc' => $customControlConfig
];
}
$parameters = [
'table' => $table,
'field' => $field,
'row' => $row,
'nameObject' => $nameObject,
'nameForm' => $nameForm,
'config' => $config,
'customControlConfig' => $customControlConfig,
];
$html .= GeneralUtility::callUserFunction($customControlConfig['userFunc'], $parameters, $this);
}
$html .= '</div>';
}
// Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post
if (count($sortableRecordUids) > 1 && $config['appearance']['useSortable']) {
$resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");';
}
$resultArray['requireJsModules'] = array_merge($resultArray['requireJsModules'], $this->requireJsModules);
// Publish the uids of the child records in the given order to the browser
$html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
. ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString(['type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']])) . '"'
. ' class="inlineRecord" />';
// Close the wrap for all inline fields (container)
$html .= '</div>';
$resultArray['html'] = $html;
return $resultArray;
}
/**
* Creates the HTML code of a general link to be used on a level of inline children.
* The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'.
*
* @param string $type The link type, values are 'newRecord', 'localize' and 'synchronize'.
* @param string $objectPrefix The "path" to the child record to create (e.g. 'data-parentPageId-partenTable-parentUid-parentField-childTable]')
* @param array $conf TCA configuration of the parent(!) field
* @return string The HTML code of the new link, wrapped in a div
*/
protected function getLevelInteractionLink($type, $objectPrefix, $conf = [])
{
$languageService = $this->getLanguageService();
$nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
$attributes = [];
switch ($type) {
case 'newRecord':
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createnew'));
$icon = 'actions-add';
$className = 'typo3-newRecordLink';
$attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
$attributes['onclick'] = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ')';
if (!empty($conf['inline']['inlineNewButtonStyle'])) {
$attributes['style'] = $conf['inline']['inlineNewButtonStyle'];
}
if (!empty($conf['appearance']['newRecordLinkAddTitle'])) {
$title = htmlspecialchars(sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createnew.link'),
$languageService->sL($GLOBALS['TCA'][$conf['foreign_table']]['ctrl']['title'])
));
} elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') {
$title = htmlspecialchars($languageService->sL($conf['appearance']['newRecordLinkTitle']));
}
break;
case 'localize':
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localizeAllRecords'));
$icon = 'actions-document-localize';
$className = 'typo3-localizationLink';
$attributes['class'] = 'btn btn-default';
$attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'localize\')';
break;
case 'synchronize':
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:synchronizeWithOriginalLanguage'));
$icon = 'actions-document-synchronize';
$className = 'typo3-synchronizationLink';
$attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
$attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'synchronize\')';
break;
default:
$title = '';
$icon = '';
$className = '';
}
// Create the link:
$icon = $icon ? $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() : '';
$link = $this->wrapWithAnchor($icon . ' ' . $title, '#', $attributes);
return '<div' . ($className ? ' class="' . $className . '"' : '') . 'title="' . $title . '">' . $link . '</div>';
}
/**
* Wraps a text with an anchor and returns the HTML representation.
*
* @param string $text The text to be wrapped by an anchor
* @param string $link The link to be used in the anchor
* @param array $attributes Array of attributes to be used in the anchor
* @return string The wrapped text as HTML representation
*/
protected function wrapWithAnchor($text, $link, $attributes = [])
{
$attributes['href'] = trim($link ?: '#');
return '<a ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>' . $text . '</a>';
}
/**
* Generate a link that opens an element browser in a new window.
* For group/db there is no way to use a "selector" like a <select>|</select>-box.
*
* @param array $inlineConfiguration TCA inline configuration of the parent(!) field
* @return string A HTML link that opens an element browser in a new window
*/
protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration)
{
$backendUser = $this->getBackendUserAuthentication();
$languageService = $this->getLanguageService();
$groupFieldConfiguration = $inlineConfiguration['selectorOrUniqueConfiguration']['config'];
$foreign_table = $inlineConfiguration['foreign_table'];
$allowed = $groupFieldConfiguration['allowed'];
$currentStructureDomObjectIdPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
$objectPrefix = $currentStructureDomObjectIdPrefix . '-' . $foreign_table;
$nameObject = $currentStructureDomObjectIdPrefix;
$mode = 'db';
$showUpload = false;
$elementBrowserEnabled = true;
if (!empty($inlineConfiguration['appearance']['createNewRelationLinkTitle'])) {
$createNewRelationText = htmlspecialchars($languageService->sL($inlineConfiguration['appearance']['createNewRelationLinkTitle']));
} else {
$createNewRelationText = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation'));
}
if (is_array($groupFieldConfiguration['appearance'])) {
if (isset($groupFieldConfiguration['appearance']['elementBrowserType'])) {
$mode = $groupFieldConfiguration['appearance']['elementBrowserType'];
}
if ($mode === 'file') {
$showUpload = true;
}
if (isset($inlineConfiguration['appearance']['fileUploadAllowed'])) {
$showUpload = (bool)$inlineConfiguration['appearance']['fileUploadAllowed'];
}
if (isset($groupFieldConfiguration['appearance']['elementBrowserAllowed'])) {
$allowed = $groupFieldConfiguration['appearance']['elementBrowserAllowed'];
}
if (isset($inlineConfiguration['appearance']['elementBrowserEnabled'])) {
$elementBrowserEnabled = (bool)$inlineConfiguration['appearance']['elementBrowserEnabled'];
}
}
// Remove any white-spaces from the allowed extension lists
$allowed = implode(',', GeneralUtility::trimExplode(',', $allowed, true));
$browserParams = '|||' . $allowed . '|' . $objectPrefix . '|inline.checkUniqueElement||inline.importElement';
$onClick = 'setFormValueOpenBrowser(' . GeneralUtility::quoteJSvalue($mode) . ', ' . GeneralUtility::quoteJSvalue($browserParams) . '); return false;';
$buttonStyle = '';
if (isset($inlineConfiguration['inline']['inlineNewRelationButtonStyle'])) {
$buttonStyle = ' style="' . $inlineConfiguration['inline']['inlineNewRelationButtonStyle'] . '"';
}
$item = '';
if ($elementBrowserEnabled) {
$item .= '
<a href="#" class="btn btn-default inlineNewRelationButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
' . $buttonStyle . ' onclick="' . htmlspecialchars($onClick) . '" title="' . $createNewRelationText . '">
' . $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render() . '
' . $createNewRelationText . '
</a>';
}
$isDirectFileUploadEnabled = (bool)$backendUser->uc['edit_docModuleUpload'];
$allowedArray = GeneralUtility::trimExplode(',', $allowed, true);
$onlineMediaAllowed = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
if (!empty($allowedArray)) {
$onlineMediaAllowed = array_intersect($allowedArray, $onlineMediaAllowed);
}
if ($showUpload && $isDirectFileUploadEnabled) {
$folder = $backendUser->getDefaultUploadFolder(
$this->data['tableName'] === 'pages' ? $this->data['vanillaUid'] : $this->data['parentPageRow']['uid'],
$this->data['tableName'],
$this->data['fieldName']
);
if (
$folder instanceof Folder
&& $folder->getStorage()->checkUserActionPermission('add', 'File')
) {
$maxFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
$item .= ' <a href="#" class="btn btn-default t3js-drag-uploader inlineNewFileUploadButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
' . $buttonStyle . '
data-dropzone-target="#' . htmlspecialchars(StringUtility::escapeCssSelector($currentStructureDomObjectIdPrefix)) . '"
data-insert-dropzone-before="1"
data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
data-file-allowed="' . htmlspecialchars($allowed) . '"
data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
data-max-file-size="' . htmlspecialchars($maxFileSize) . '"
>';
$item .= $this->iconFactory->getIcon('actions-upload', Icon::SIZE_SMALL)->render() . ' ';
$item .= htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.select-and-submit'));
$item .= '</a>';
$this->requireJsModules[] = ['TYPO3/CMS/Backend/DragUploader' => 'function(dragUploader){dragUploader.initialize()}'];
if (!empty($onlineMediaAllowed)) {
$buttonStyle = '';
if (isset($inlineConfiguration['inline']['inlineOnlineMediaAddButtonStyle'])) {
$buttonStyle = ' style="' . $inlineConfiguration['inline']['inlineOnlineMediaAddButtonStyle'] . '"';
}
$this->requireJsModules[] = 'TYPO3/CMS/Backend/OnlineMedia';
$buttonText = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.button'));
$placeholder = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.placeholder'));
$buttonSubmit = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.submit'));
$allowedMediaUrl = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.allowEmbedSources'));
$item .= '
<span class="btn btn-default t3js-online-media-add-btn ' . $this->inlineData['config'][$nameObject]['md5'] . '"
' . $buttonStyle . '
data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
data-online-media-allowed="' . htmlspecialchars(implode(',', $onlineMediaAllowed)) . '"
data-online-media-allowed-help-text="' . $allowedMediaUrl . '"
data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
title="' . $buttonText . '"
data-btn-submit="' . $buttonSubmit . '"
data-placeholder="' . $placeholder . '"
>
' . $this->iconFactory->getIcon('actions-online-media-add', Icon::SIZE_SMALL)->render() . '
' . $buttonText . '</span>';
}
}
}
$item = '<div class="form-control-wrap">' . $item . '</div>';
$allowedList = '';
$allowedLabelKey = ($mode === 'file') ? 'allowedFileExtensions' : 'allowedRelations';
$allowedLabel = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . $allowedLabelKey));
foreach ($allowedArray as $allowedItem) {
$allowedList .= '<span class="label label-success">' . strtoupper($allowedItem) . '</span> ';
}
if (!empty($allowedList)) {
$item .= '<div class="help-block">' . $allowedLabel . '<br>' . $allowedList . '</div>';
}
$item = '<div class="form-group t3js-formengine-validation-marker">' . $item . '</div>';
return $item;
}
/**
* Get a selector as used for the select type, to select from all available
* records and to create a relation to the embedding record (e.g. like MM).
*
* @param array $config TCA inline configuration of the parent(!) field
* @param array $uniqueIds The uids that have already been used and should be unique
* @return string A HTML <select> box with all possible records
*/
protected function renderPossibleRecordsSelectorTypeSelect(array $config, array $uniqueIds)
{
$possibleRecords = $config['selectorOrUniquePossibleRecords'];
$nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
// Create option tags:
$opt = [];
foreach ($possibleRecords as $p) {
if (!in_array($p[1], $uniqueIds)) {
$opt[] = '<option value="' . htmlspecialchars($p[1]) . '">' . htmlspecialchars($p[0]) . '</option>';
}
}
// Put together the selector box:
$size = (int)$config['size'];
$size = $config['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($possibleRecords) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax']) : $size;
$onChange = 'return inline.importNewRecord(' . GeneralUtility::quoteJSvalue($nameObject . '-' . $config['foreign_table']) . ')';
$item = '
<select id="' . $nameObject . '-' . $config['foreign_table'] . '_selector" class="form-control"' . ($size ? ' size="' . $size . '"' : '')
. ' onchange="' . htmlspecialchars($onChange) . '"' . ($config['foreign_unique'] ? ' isunique="isunique"' : '') . '>
' . implode('', $opt) . '
</select>';
if ($size <= 1) {
// Add a "Create new relation" link for adding new relations
// This is necessary, if the size of the selector is "1" or if
// there is only one record item in the select-box, that is selected by default
// The selector-box creates a new relation on using an onChange event (see some line above)
if (!empty($config['appearance']['createNewRelationLinkTitle'])) {
$createNewRelationText = htmlspecialchars($this->getLanguageService()->sL($config['appearance']['createNewRelationLinkTitle']));
} else {
$createNewRelationText = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation'));
}
$item .= '
<span class="input-group-btn">
<a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onChange) . '" title="' . $createNewRelationText . '">
' . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . $createNewRelationText . '
</a>
</span>';
} else {
$item .= '
<span class="input-group-btn btn"></span>';
}
// Wrap the selector and add a spacer to the bottom
$item = '<div class="input-group form-group t3js-formengine-validation-marker ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>';
return $item;
}
/**
* Extracts FlexForm parts of a form element name like
* data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
* Helper method used in inline
*
* @param string $formElementName The form element name
* @return array|null
*/
protected function extractFlexFormParts($formElementName)
{
$flexFormParts = null;
$matches = [];
if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
$flexFormParts = GeneralUtility::trimExplode(
'][',
trim($matches[1], '[]')
);
}
return $flexFormParts;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,674 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedContentEditException;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Render a single inline record relation.
*
* This container is called by InlineControlContainer to render single existing records.
* Furthermore it is called by FormEngine for an incoming ajax request to expand an existing record
* or to create a new one.
*
* This container creates the outer HTML of single inline records - eg. drag and drop and delete buttons.
* For rendering of the record itself processing is handed over to FullRecordContainer.
*/
class InlineRecordContainer extends AbstractContainer
{
/**
* Inline data array used for JSON output
*
* @var array
*/
protected $inlineData = [];
/**
* @var InlineStackProcessor
*/
protected $inlineStackProcessor;
/**
* Array containing instances of hook classes called once for IRRE objects
*
* @var array
*/
protected $hookObjects = [];
/**
* @var IconFactory
*/
protected $iconFactory;
/**
* Default constructor
*
* @param NodeFactory $nodeFactory
* @param array $data
*/
public function __construct(NodeFactory $nodeFactory, array $data)
{
parent::__construct($nodeFactory, $data);
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$this->inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$this->initHookObjects();
}
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws AccessDeniedContentEditException
*/
public function render()
{
$data = $this->data;
$this->inlineData = $data['inlineData'];
$inlineStackProcessor = $this->inlineStackProcessor;
$inlineStackProcessor->initializeByGivenStructure($data['inlineStructure']);
$record = $data['databaseRow'];
$inlineConfig = $data['inlineParentConfig'];
$foreignTable = $inlineConfig['foreign_table'];
$resultArray = $this->initializeResultArray();
// Send a mapping information to the browser via JSON:
// e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
$formPrefix = $inlineStackProcessor->getCurrentStructureFormPrefix();
$domObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
$this->inlineData['map'][$formPrefix] = $domObjectId;
$resultArray['inlineData'] = $this->inlineData;
// If there is a selector field, normalize it:
if (!empty($inlineConfig['foreign_selector'])) {
$foreign_selector = $inlineConfig['foreign_selector'];
$valueToNormalize = $record[$foreign_selector];
if (is_array($record[$foreign_selector])) {
// @todo: this can be kicked again if always prepared rows are handled here
$valueToNormalize = implode(',', $record[$foreign_selector]);
}
$record[$foreign_selector] = $this->normalizeUid($valueToNormalize);
}
// Get the current naming scheme for DOM name/id attributes:
$appendFormFieldNames = '[' . $foreignTable . '][' . $record['uid'] . ']';
$objectId = $domObjectId . '-' . $foreignTable . '-' . $record['uid'];
$class = '';
$html = '';
$combinationHtml = '';
$isNewRecord = $data['command'] === 'new';
$hiddenField = '';
if (isset($data['processedTca']['ctrl']['enablecolumns']['disabled'])) {
$hiddenField = $data['processedTca']['ctrl']['enablecolumns']['disabled'];
}
if (!$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
if ($isNewRecord || $data['isInlineChildExpanded']) {
// Render full content ONLY IF this is an AJAX request, a new record, or the record is not collapsed
$combinationHtml = '';
if (isset($data['combinationChild'])) {
$combinationChild = $this->renderCombinationChild($data, $appendFormFieldNames);
$combinationHtml = $combinationChild['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild, false);
}
$childArray = $this->renderChild($data);
$html = $childArray['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
} else {
// This string is the marker for the JS-function to check if the full content has already been loaded
$html = '<!--notloaded-->';
}
if ($isNewRecord) {
// Add pid of record as hidden field
$html .= '<input type="hidden" name="data' . htmlspecialchars($appendFormFieldNames)
. '[pid]" value="' . htmlspecialchars($record['pid']) . '"/>';
// Tell DataHandler this record is expanded
$ucFieldName = 'uc[inlineView]'
. '[' . $data['inlineTopMostParentTableName'] . ']'
. '[' . $data['inlineTopMostParentUid'] . ']'
. $appendFormFieldNames;
$html .= '<input type="hidden" name="' . htmlspecialchars($ucFieldName)
. '" value="' . (int)$data['isInlineChildExpanded'] . '" />';
} else {
// Set additional field for processing for saving
$html .= '<input type="hidden" name="cmd' . htmlspecialchars($appendFormFieldNames)
. '[delete]" value="1" disabled="disabled" />';
if (!$data['isInlineChildExpanded'] && !empty($hiddenField)) {
$checked = !empty($record[$hiddenField]) ? ' checked="checked"' : '';
$html .= '<input type="checkbox" data-formengine-input-name="data'
. htmlspecialchars($appendFormFieldNames)
. '[' . htmlspecialchars($hiddenField) . ']" value="1"' . $checked . ' />';
$html .= '<input type="input" name="data' . htmlspecialchars($appendFormFieldNames)
. '[' . htmlspecialchars($hiddenField) . ']" value="' . htmlspecialchars($record[$hiddenField]) . '" />';
}
}
// If this record should be shown collapsed
$class = $data['isInlineChildExpanded'] ? 'panel-visible' : 'panel-collapsed';
}
$hiddenFieldHtml = implode(LF, $resultArray['additionalHiddenFields'] ?? []);
if ($inlineConfig['renderFieldsOnly']) {
// Render "body" part only
$html = $html . $combinationHtml;
} else {
// Render header row and content (if expanded)
if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$class .= ' t3-form-field-container-inline-placeHolder';
}
if (!empty($hiddenField) && isset($record[$hiddenField]) && (int)$record[$hiddenField]) {
$class .= ' t3-form-field-container-inline-hidden';
}
$class .= ($isNewRecord ? ' inlineIsNewRecord' : '');
$tableUniqueOriginalValue = '';
if (isset($data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']])) {
$uniqueValueValues = $data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']];
// in case of site_language we don't have the full form engine options, so fallbacks need to be taken into account
$tableUniqueOriginalValue = ($uniqueValueValues['table'] ?? $foreignTable) . '_' . ($uniqueValueValues['uid'] ?? $uniqueValueValues);
}
$html = '
<div class="panel panel-default panel-condensed ' . trim($class) . '" id="' . htmlspecialchars($objectId) . '_div" data-table-unique-original-value="' . htmlspecialchars($tableUniqueOriginalValue) . '">
<div class="panel-heading" data-toggle="formengine-inline" id="' . htmlspecialchars($objectId) . '_header" data-expandSingle="' . ($inlineConfig['appearance']['expandSingle'] ? 1 : 0) . '">
<div class="form-irre-header">
<div class="form-irre-header-cell form-irre-header-icon">
<span class="caret"></span>
</div>
' . $this->renderForeignRecordHeader($data) . '
</div>
</div>
<div class="panel-collapse" id="' . htmlspecialchars($objectId) . '_fields">' . $html . $hiddenFieldHtml . $combinationHtml . '</div>
</div>';
}
$resultArray['html'] = $html;
return $resultArray;
}
/**
* Render inner child
*
* @param array $data
* @return array Result array
*/
protected function renderChild(array $data)
{
$domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
$data['tabAndInlineStack'][] = [
'inline',
$domObjectId . '-' . $data['tableName'] . '-' . $data['databaseRow']['uid'],
];
// @todo: ugly construct ...
$data['inlineData'] = $this->inlineData;
$data['renderType'] = 'fullRecordContainer';
return $this->nodeFactory->create($data)->render();
}
/**
* Render child child
*
* Render a table with FormEngine, that occurs on an intermediate table but should be editable directly,
* so two tables are combined (the intermediate table with attributes and the sub-embedded table).
* -> This is a direct embedding over two levels!
*
* @param array $data
* @param string $appendFormFieldNames The [<table>][<uid>] of the parent record (the intermediate table)
* @return array Result array
*/
protected function renderCombinationChild(array $data, $appendFormFieldNames)
{
$childData = $data['combinationChild'];
$parentConfig = $data['inlineParentConfig'];
// If field is set to readOnly, set all fields of the relation to readOnly as well
if (isset($parentConfig['readOnly']) && $parentConfig['readOnly']) {
foreach ($childData['processedTca']['columns'] as $columnName => $columnConfiguration) {
$childData['processedTca']['columns'][$columnName]['config']['readOnly'] = true;
}
}
$resultArray = $this->initializeResultArray();
// Display Warning FlashMessage if it is not suppressed
if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
$combinationWarningMessage = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.inline_use_combination';
if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
$combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
}
$message = $this->getLanguageService()->sL($combinationWarningMessage);
$markup = [];
// @TODO: This is not a FlashMessage! The markup must be changed and special CSS
// @TODO: should be created, in order to prevent confusion.
$markup[] = '<div class="alert alert-warning">';
$markup[] = ' <div class="media">';
$markup[] = ' <div class="media-left">';
$markup[] = ' <span class="fa-stack fa-lg">';
$markup[] = ' <i class="fa fa-circle fa-stack-2x"></i>';
$markup[] = ' <i class="fa fa-exclamation fa-stack-1x"></i>';
$markup[] = ' </span>';
$markup[] = ' </div>';
$markup[] = ' <div class="media-body">';
$markup[] = ' <div class="alert-message">' . htmlspecialchars($message) . '</div>';
$markup[] = ' </div>';
$markup[] = ' </div>';
$markup[] = '</div>';
$resultArray['html'] = implode(LF, $markup);
}
$childArray = $this->renderChild($childData);
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
// If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
if ($childData['command'] === 'new') {
$comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
$resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
}
// If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
$parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
$resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
}
return $resultArray;
}
/**
* Renders the HTML header for a foreign record, such as the title, toggle-function, drag'n'drop, etc.
* Later on the command-icons are inserted here.
*
* @param array $data Current data
* @return string The HTML code of the header
*/
protected function renderForeignRecordHeader(array $data)
{
$languageService = $this->getLanguageService();
$inlineConfig = $data['inlineParentConfig'];
$foreignTable = $inlineConfig['foreign_table'];
$rec = $data['databaseRow'];
// Init:
$domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
$objectId = $domObjectId . '-' . $foreignTable . '-' . $rec['uid'];
$recordTitle = $data['recordTitle'];
if (!empty($recordTitle)) {
// The user function may return HTML, therefore we can't escape it
if (empty($data['processedTca']['ctrl']['formattedLabel_userFunc'])) {
$recordTitle = BackendUtility::getRecordTitlePrep($recordTitle);
}
} else {
$recordTitle = '<em>[' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>';
}
$altText = BackendUtility::getRecordIconAltText($rec, $foreignTable);
$iconImg = '<span title="' . $altText . '" id="' . htmlspecialchars($objectId) . '_icon' . '">' . $this->iconFactory->getIconForRecord($foreignTable, $rec, Icon::SIZE_SMALL)->render() . '</span>';
$label = '<span id="' . $objectId . '_label">' . $recordTitle . '</span>';
$ctrl = $this->renderForeignRecordHeaderControl($data);
$thumbnail = false;
// Renders a thumbnail for the header
if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] && !empty($inlineConfig['appearance']['headerThumbnail']['field'])) {
$fieldValue = $rec[$inlineConfig['appearance']['headerThumbnail']['field']];
$fileUid = $fieldValue[0]['uid'];
if (!empty($fileUid)) {
try {
$fileObject = ResourceFactory::getInstance()->getFileObject($fileUid);
} catch (\InvalidArgumentException $e) {
$fileObject = null;
}
if ($fileObject && $fileObject->isMissing()) {
$thumbnail .= '<span class="label label-danger">'
. htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
. '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
} elseif ($fileObject) {
$imageSetup = $inlineConfig['appearance']['headerThumbnail'];
unset($imageSetup['field']);
if (!empty($rec['crop'])) {
$imageSetup['crop'] = $rec['crop'];
}
$imageSetup = array_merge(['width' => '45', 'height' => '45c'], $imageSetup);
if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
&& GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileObject->getExtension())) {
$processedImage = $fileObject->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, $imageSetup);
// Only use a thumbnail if the processing process was successful by checking if image width is set
if ($processedImage->getProperty('width')) {
$imageUrl = $processedImage->getPublicUrl(true);
$thumbnail = '<img src="' . $imageUrl . '" ' .
'width="' . $processedImage->getProperty('width') . '" ' .
'height="' . $processedImage->getProperty('height') . '" ' .
'alt="' . htmlspecialchars($altText) . '" ' .
'title="' . htmlspecialchars($altText) . '">';
}
} else {
$thumbnail = '';
}
}
}
}
if (!empty($inlineConfig['appearance']['headerThumbnail']['field']) && $thumbnail) {
$mediaContainer = '<div class="form-irre-header-cell form-irre-header-thumbnail" id="' . $objectId . '_thumbnailcontainer">' . $thumbnail . '</div>';
} else {
$mediaContainer = '<div class="form-irre-header-cell form-irre-header-icon" id="' . $objectId . '_iconcontainer">' . $iconImg . '</div>';
}
$header = $mediaContainer . '
<div class="form-irre-header-cell form-irre-header-body">' . $label . '</div>
<div class="form-irre-header-cell form-irre-header-control t3js-formengine-irre-control">' . $ctrl . '</div>';
return $header;
}
/**
* Render the control-icons for a record header (create new, sorting, delete, disable/enable).
* Most of the parts are copy&paste from TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList and
* modified for the JavaScript calls here
*
* @param array $data Current data
* @return string The HTML code with the control-icons
*/
protected function renderForeignRecordHeaderControl(array $data)
{
$rec = $data['databaseRow'];
$inlineConfig = $data['inlineParentConfig'];
$foreignTable = $inlineConfig['foreign_table'];
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUserAuthentication();
// Initialize:
$cells = [
'edit' => '',
'hide' => '',
'delete' => '',
'info' => '',
'new' => '',
'sort.up' => '',
'sort.down' => '',
'dragdrop' => '',
'localize' => '',
'locked' => '',
];
$isNewItem = strpos($rec['uid'], 'NEW') === 0;
$isParentReadOnly = isset($inlineConfig['readOnly']) && $inlineConfig['readOnly'];
$isParentExisting = MathUtility::canBeInterpretedAsInteger($data['inlineParentUid']);
$tcaTableCtrl = $GLOBALS['TCA'][$foreignTable]['ctrl'];
$tcaTableCols = $GLOBALS['TCA'][$foreignTable]['columns'];
$isPagesTable = $foreignTable === 'pages';
$isSysFileReferenceTable = $foreignTable === 'sys_file_reference';
$enableManualSorting = $tcaTableCtrl['sortby'] || $inlineConfig['MM'] || !$data['isOnSymmetricSide']
&& $inlineConfig['foreign_sortby'] || $data['isOnSymmetricSide'] && $inlineConfig['symmetric_sortby'];
$nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
$nameObjectFt = $nameObject . '-' . $foreignTable;
$nameObjectFtId = $nameObjectFt . '-' . $rec['uid'];
$calcPerms = $backendUser->calcPerms(BackendUtility::readPageAccess($rec['pid'], $backendUser->getPagePermsClause(Permission::PAGE_SHOW)));
// If the listed table is 'pages' we have to request the permission settings for each page:
$localCalcPerms = false;
if ($isPagesTable) {
$localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid']));
}
// This expresses the edit permissions for this particular element:
$permsEdit = $isPagesTable && $localCalcPerms & Permission::PAGE_EDIT || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT;
// Controls: Defines which controls should be shown
$enabledControls = $inlineConfig['appearance']['enabledControls'];
// Hook: Can disable/enable single controls for specific child records:
foreach ($this->hookObjects as $hookObj) {
/** @var InlineElementHookInterface $hookObj */
$hookObj->renderForeignRecordHeaderControl_preProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $enabledControls);
}
if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$cells['localize'] = '<span title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize.isLocalizable')) . '">
' . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render() . '
</span>';
}
// "Info": (All records)
// @todo: hardcoded sys_file!
if ($rec['table_local'] === 'sys_file') {
$uid = $rec['uid_local'][0]['uid'];
$table = '_FILE';
} else {
$uid = $rec['uid'];
$table = $foreignTable;
}
if ($enabledControls['info']) {
if ($isNewItem) {
$cells['info'] = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
} else {
$cells['info'] = '
<a class="btn btn-default" href="#" onclick="' . htmlspecialchars('top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($uid) . '); return false;') . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')) . '">
' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '
</a>';
}
}
// If the table is NOT a read-only table, then show these links:
if (!$isParentReadOnly && !$tcaTableCtrl['readOnly'] && !$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
// "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row or if default values can depend on previous record):
if ($enabledControls['new'] && ($enableManualSorting || $tcaTableCtrl['useColumnsForDefaultValues'])) {
if (!$isPagesTable && $calcPerms & Permission::CONTENT_EDIT || $isPagesTable && $calcPerms & Permission::PAGE_NEW) {
$onClick = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ',' . GeneralUtility::quoteJSvalue($rec['uid']) . ')';
$style = '';
if ($inlineConfig['inline']['inlineNewButtonStyle']) {
$style = ' style="' . $inlineConfig['inline']['inlineNewButtonStyle'] . '"';
}
$cells['new'] = '
<a class="btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'] . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:new' . ($isPagesTable ? 'Page' : 'Record'))) . '" ' . $style . '>
' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page-new' : 'add'), Icon::SIZE_SMALL)->render() . '
</a>';
}
}
// "Up/Down" links
if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) {
// Up
$onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'1\')';
$icon = 'actions-move-up';
$class = '';
if ($inlineConfig['inline']['first'] == $rec['uid']) {
$class = ' disabled';
$icon = 'empty-empty';
}
$cells['sort.up'] = '
<a class="btn btn-default sortingUp' . $class . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveUp')) . '">
' . $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() . '
</a>';
// Down
$onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'-1\')';
$icon = 'actions-move-down';
$class = '';
if ($inlineConfig['inline']['last'] == $rec['uid']) {
$class = ' disabled';
$icon = 'empty-empty';
}
$cells['sort.down'] = '
<a class="btn btn-default sortingDown' . $class . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveDown')) . '">
' . $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() . '
</a>';
}
// "Edit" link:
if (($rec['table_local'] === 'sys_file') && !$isNewItem && $backendUser->check('tables_modify', 'sys_file_metadata')) {
$sys_language_uid = 0;
if (!empty($rec['sys_language_uid'])) {
$sys_language_uid = $rec['sys_language_uid'][0];
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_file_metadata');
$recordInDatabase = $queryBuilder
->select('uid')
->from('sys_file_metadata')
->where(
$queryBuilder->expr()->eq(
'file',
$queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'sys_language_uid',
$queryBuilder->createNamedParameter($sys_language_uid, \PDO::PARAM_INT)
)
)
->setMaxResults(1)
->execute()
->fetch();
if (!empty($recordInDatabase)) {
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', [
'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit',
'returnUrl' => $this->data['returnUrl']
]);
$title = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata');
$cells['edit'] = '
<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($title) . '">
' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '
</a>';
}
}
// "Delete" link:
if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & Permission::PAGE_DELETE
|| !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT
|| $isSysFileReferenceTable && $calcPerms & Permission::PAGE_EDIT)
) {
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete'));
$icon = $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render();
$cells['delete'] = '<a href="#" class="btn btn-default t3js-editform-delete-inline-record" data-objectid="' . htmlspecialchars($nameObjectFtId) . '" title="' . $title . '">' . $icon . '</a>';
}
// "Hide/Unhide" links:
$hiddenField = $tcaTableCtrl['enablecolumns']['disabled'];
if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))) {
$onClick = 'return inline.enableDisableRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ',' .
GeneralUtility::quoteJSvalue($hiddenField) . ')';
$className = 't3js-' . $nameObjectFtId . '_disabled';
if ($rec[$hiddenField]) {
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : '')));
$cells['hide'] = '
<a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="
' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">
' . $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '
</a>';
} else {
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : '')));
$cells['hide'] = '
<a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="
' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">
' . $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '
</a>';
}
}
// Drag&Drop Sorting: Sortable handler for script.aculo.us
if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $inlineConfig['appearance']['useSortable']) {
$cells['dragdrop'] = '
<span class="btn btn-default sortableHandle" data-id="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move')) . '">
' . $this->iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '
</span>';
}
} elseif ($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] && $isParentExisting) {
if ($enabledControls['localize'] && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
$cells['localize'] = '
<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize')) . '">
' . $this->iconFactory->getIcon('actions-document-localize', Icon::SIZE_SMALL)->render() . '
</a>';
}
}
// If the record is edit-locked by another user, we will show a little warning sign:
if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) {
$cells['locked'] = '
<a class="btn btn-default" href="#" data-toggle="tooltip" data-title="' . htmlspecialchars($lockInfo['msg']) . '">
' . '<span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</span>' . '
</a>';
}
// Hook: Post-processing of single controls for specific child records:
foreach ($this->hookObjects as $hookObj) {
$hookObj->renderForeignRecordHeaderControl_postProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $cells);
}
$out = '';
if (!empty($cells['edit']) || !empty($cells['hide']) || !empty($cells['delete'])) {
$out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['edit'] . $cells['hide'] . $cells['delete'] . '</div>';
unset($cells['edit'], $cells['hide'], $cells['delete']);
}
if (!empty($cells['info']) || !empty($cells['new']) || !empty($cells['sort.up']) || !empty($cells['sort.down']) || !empty($cells['dragdrop'])) {
$out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['info'] . $cells['new'] . $cells['sort.up'] . $cells['sort.down'] . $cells['dragdrop'] . '</div>';
unset($cells['info'], $cells['new'], $cells['sort.up'], $cells['sort.down'], $cells['dragdrop']);
}
if (!empty($cells['localize'])) {
$out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['localize'] . '</div>';
unset($cells['localize']);
}
if (!empty($cells)) {
$out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
}
return $out;
}
/**
* Normalize a relation "uid" published by transferData, like "1|Company%201"
*
* @param string $string A transferData reference string, containing the uid
* @return string The normalized uid
*/
protected function normalizeUid($string)
{
$parts = explode('|', $string);
return $parts[0];
}
/**
* Initialized the hook objects for this class.
* Each hook object has to implement the interface
* \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
*
* @throws \UnexpectedValueException
*/
protected function initHookObjects()
{
$this->hookObjects = [];
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'] ?? [] as $className) {
$processObject = GeneralUtility::makeInstance($className);
if (!$processObject instanceof InlineElementHookInterface) {
throw new \UnexpectedValueException($className . ' must implement interface ' . InlineElementHookInterface::class, 1202072000);
}
$this->hookObjects[] = $processObject;
}
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,89 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Render a given list of field of a TCA table.
*
* This is an entry container called from FormEngine to handle a
* list of specific fields. Access rights are checked here and globalOption array
* is prepared for further processing of single fields by PaletteAndSingleContainer.
*/
class ListOfFieldsContainer extends AbstractContainer
{
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$table = $this->data['tableName'];
$fieldListToRender = $this->data['fieldListToRender'];
$recordTypeValue = $this->data['recordTypeValue'];
// Load the description content for the table if requested
if (!empty($GLOBALS['TCA'][$table]['interface']['always_description'])) {
$languageService = $this->getLanguageService();
$languageService->loadSingleTableDescription($table);
}
$fieldListToRender = array_unique(GeneralUtility::trimExplode(',', $fieldListToRender, true));
$fieldsByShowitem = $this->data['processedTca']['types'][$recordTypeValue]['showitem'];
$fieldsByShowitem = GeneralUtility::trimExplode(',', $fieldsByShowitem, true);
$finalFieldsList = [];
foreach ($fieldListToRender as $fieldName) {
foreach ($fieldsByShowitem as $fieldByShowitem) {
$fieldByShowitemArray = $this->explodeSingleFieldShowItemConfiguration($fieldByShowitem);
if ($fieldByShowitemArray['fieldName'] === $fieldName) {
$finalFieldsList[] = implode(';', $fieldByShowitemArray);
break;
}
if ($fieldByShowitemArray['fieldName'] === '--palette--'
&& isset($this->data['processedTca']['palettes'][$fieldByShowitemArray['paletteName']]['showitem'])
&& is_string($this->data['processedTca']['palettes'][$fieldByShowitemArray['paletteName']]['showitem'])
) {
$paletteName = $fieldByShowitemArray['paletteName'];
$paletteFields = GeneralUtility::trimExplode(',', $this->data['processedTca']['palettes'][$paletteName]['showitem'], true);
foreach ($paletteFields as $paletteField) {
$paletteFieldArray = $this->explodeSingleFieldShowItemConfiguration($paletteField);
if ($paletteFieldArray['fieldName'] === $fieldName) {
$finalFieldsList[] = implode(';', $paletteFieldArray);
break;
}
}
}
}
}
$options = $this->data;
$options['fieldsArray'] = $finalFieldsList;
$options['renderType'] = 'paletteAndSingleContainer';
return $this->nodeFactory->create($options)->render();
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,38 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
/**
* Handle a record that has no tabs.
*
* This container is called by FullRecordContainer and just wraps the output
* of PaletteAndSingleContainer in some HTML.
*/
class NoTabsContainer extends AbstractContainer
{
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$options = $this->data;
$options['renderType'] = 'paletteAndSingleContainer';
$resultArray = $this->nodeFactory->create($options)->render();
$resultArray['html'] = '<div class="tab-content">' . $resultArray['html'] . '</div>';
return $resultArray;
}
}
@@ -0,0 +1,169 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* Render header and footer row.
*
* This is an entry container called from controllers.
* It wraps the title and a footer around the main html.
* It either calls a FullRecordContainer or ListOfFieldsContainer to render
* a full record or only some fields from a full record.
*/
class OuterWrapContainer extends AbstractContainer
{
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUserAuthentication();
$table = $this->data['tableName'];
$row = $this->data['databaseRow'];
$options = $this->data;
if (empty($this->data['fieldListToRender'])) {
$options['renderType'] = 'fullRecordContainer';
} else {
$options['renderType'] = 'listOfFieldsContainer';
}
$result = $this->nodeFactory->create($options)->render();
$childHtml = $result['html'];
$recordPath = '';
// @todo: what is this >= 0 check for? wsol cases?!
if ($this->data['effectivePid'] >= 0) {
$permissionsClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
$recordPath = BackendUtility::getRecordPath($this->data['effectivePid'], $permissionsClause, 15);
}
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$icon = '<span title="' . htmlspecialchars($recordPath) . '">' . $iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>';
// @todo: Could this be done in a more clever way? Does it work at all?
$tableTitle = $languageService->sL($this->data['processedTca']['ctrl']['title']);
if ($this->data['command'] === 'new') {
$newOrUid = ' <span class="typo3-TCEforms-newToken">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.new')) . '</span>';
// @todo: There is quite some stuff do to for WS overlays ...
$workspacedPageRecord = BackendUtility::getRecordWSOL('pages', $this->data['effectivePid'], 'title');
$pageTitle = BackendUtility::getRecordTitle('pages', $workspacedPageRecord, true, false);
if ($table === 'pages') {
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNewPage'));
$pageTitle = sprintf($label, $tableTitle);
} else {
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNewRecord'));
if ($this->data['effectivePid'] === 0) {
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNewRecordRootLevel'));
}
$pageTitle = sprintf($label, $tableTitle, $pageTitle);
}
} else {
$icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']);
$newOrUid = ' <span class="typo3-TCEforms-recUid">[' . htmlspecialchars($row['uid']) . ']</span>';
// @todo: getRecordTitlePrep applies an htmlspecialchars here
$recordLabel = BackendUtility::getRecordTitlePrep($this->data['recordTitle']);
if ($table === 'pages') {
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editPage'));
$pageTitle = sprintf($label, $tableTitle, $recordLabel);
} else {
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editRecord'));
$workspacedPageRecord = BackendUtility::getRecordWSOL('pages', $row['pid'], 'uid,title');
$pageTitle = BackendUtility::getRecordTitle('pages', $workspacedPageRecord, true, false);
if (empty($recordLabel)) {
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editRecordNoTitle'));
}
if ($this->data['effectivePid'] === 0) {
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editRecordRootLevel'));
}
if (!empty($recordLabel)) {
// Use record title and prepend an edit label.
$pageTitle = sprintf($label, $tableTitle, $recordLabel, $pageTitle);
} else {
// Leave out the record title since it is not set.
$pageTitle = sprintf($label, $tableTitle, $pageTitle);
}
}
}
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
'EXT:backend/Resources/Private/Templates/OuterWrapContainer.html'
));
$descriptionColumn = !empty($this->data['processedTca']['ctrl']['descriptionColumn'])
? $this->data['processedTca']['ctrl']['descriptionColumn'] : null;
if ($descriptionColumn !== null) {
$view->assign('recordDescription', $this->data['databaseRow'][$descriptionColumn]);
}
$readOnlyRecord = !empty($this->data['processedTca']['ctrl']['readOnly'])
? (bool)$this->data['processedTca']['ctrl']['readOnly'] : null;
if ($readOnlyRecord === true) {
$view->assign('recordReadonly', true);
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$result = $this->mergeChildReturnIntoExistingResult($result, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$result = $this->mergeChildReturnIntoExistingResult($result, $fieldWizardResult, false);
$view->assignMultiple([
'pageTitle' => $pageTitle,
'fieldInformationHtml' => $fieldInformationHtml,
'fieldWizardHtml' => $fieldWizardHtml,
'childHtml' => $childHtml,
'icon' => $icon,
'tableTitle' => $tableTitle,
'newOrUid' => $newOrUid,
'isNewRecord' => $this->data['command'] === 'new'
]);
$result['html'] = $view->render();
return $result;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,431 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Handle palettes and single fields.
*
* This container is called by TabsContainer, NoTabsContainer and ListOfFieldsContainer.
*
* This container mostly operates on TCA showItem of a specific type - the value is
* coming in from upper containers as "fieldArray". It handles palettes with all its
* different options and prepares rendering of single fields for the SingleFieldContainer.
*/
class PaletteAndSingleContainer extends AbstractContainer
{
/**
* Final result array accumulating results from children and final HTML
*
* @var array
*/
protected $resultArray = [];
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
/*
* The first code block creates a target structure array to later create the final
* HTML string. The single fields and sub containers are rendered here already and
* other parts of the return array from children except html are accumulated in
* $this->resultArray
*
$targetStructure = array(
0 => array(
'type' => 'palette',
'fieldName' => 'palette1',
'fieldLabel' => 'palette1',
'elements' => array(
0 => array(
'type' => 'single',
'fieldName' => 'paletteName',
'fieldLabel' => 'element1',
'fieldHtml' => 'element1',
),
1 => array(
'type' => 'linebreak',
),
2 => array(
'type' => 'single',
'fieldName' => 'paletteName',
'fieldLabel' => 'element2',
'fieldHtml' => 'element2',
),
),
),
1 => array(
'type' => 'single',
'fieldName' => 'element3',
'fieldLabel' => 'element3',
'fieldHtml' => 'element3',
),
2 => array(
'type' => 'palette2',
'fieldName' => 'palette2',
'fieldLabel' => '', // Palettes may not have a label
'elements' => array(
0 => array(
'type' => 'single',
'fieldName' => 'element4',
'fieldLabel' => 'element4',
'fieldHtml' => 'element4',
),
1 => array(
'type' => 'linebreak',
),
2 => array(
'type' => 'single',
'fieldName' => 'element5',
'fieldLabel' => 'element5',
'fieldHtml' => 'element5',
),
),
),
);
*/
// Create an intermediate structure of rendered sub elements and elements nested in palettes
$targetStructure = [];
$mainStructureCounter = -1;
$fieldsArray = $this->data['fieldsArray'];
$this->resultArray = $this->initializeResultArray();
foreach ($fieldsArray as $fieldString) {
$fieldConfiguration = $this->explodeSingleFieldShowItemConfiguration($fieldString);
$fieldName = $fieldConfiguration['fieldName'];
if ($fieldName === '--palette--') {
$paletteElementArray = $this->createPaletteContentArray($fieldConfiguration['paletteName']);
if (!empty($paletteElementArray)) {
$mainStructureCounter++;
$paletteLabel = $fieldConfiguration['fieldLabel'];
if ($paletteLabel === null
&& !empty($this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['label'])
) {
// If there is no label in ['types']['aType']['showitem'] for this palette: "--palette--;;aPalette" but
// not "--palette--;LLL:aLabelReference;aPalette", then use ['palettes']['aPalette']['label'] if given.
$paletteLabel = $this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['label'];
}
$targetStructure[$mainStructureCounter] = [
'type' => 'palette',
'fieldName' => $fieldConfiguration['paletteName'],
'fieldLabel' => $languageService->sL($paletteLabel),
'elements' => $paletteElementArray,
];
}
} else {
if (!is_array($this->data['processedTca']['columns'][$fieldName])) {
continue;
}
$options = $this->data;
$options['fieldName'] = $fieldName;
$options['renderType'] = 'singleFieldContainer';
$childResultArray = $this->nodeFactory->create($options)->render();
if (!empty($childResultArray['html'])) {
$mainStructureCounter++;
$fieldLabel = '';
if (!empty($this->data['processedTca']['columns'][$fieldName]['label'])) {
$fieldLabel = $this->data['processedTca']['columns'][$fieldName]['label'];
}
$targetStructure[$mainStructureCounter] = [
'type' => 'single',
'fieldName' => $fieldConfiguration['fieldName'],
'fieldLabel' => $fieldLabel,
'fieldHtml' => $childResultArray['html'],
];
}
$this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray, false);
}
}
// Compile final content
$content = [];
foreach ($targetStructure as $element) {
if ($element['type'] === 'palette') {
$paletteName = $element['fieldName'];
$paletteElementsHtml = $this->renderInnerPaletteContent($element);
$isHiddenPalette = !empty($this->data['processedTca']['palettes'][$paletteName]['isHiddenPalette']);
$paletteElementsHtml = '<div class="row">' . $paletteElementsHtml . '</div>';
$content[] = $this->fieldSetWrap($paletteElementsHtml, $isHiddenPalette, $element['fieldLabel']);
} else {
// Return raw HTML only in case of user element with no wrapping requested
if ($this->isUserNoTableWrappingField($element)) {
$content[] = $element['fieldHtml'];
} else {
$content[] = $this->fieldSetWrap($this->wrapSingleFieldContentWithLabelAndOuterDiv($element));
}
}
}
$finalResultArray = $this->resultArray;
$finalResultArray['html'] = implode(LF, $content);
return $finalResultArray;
}
/**
* Render single fields of a given palette
*
* @param string $paletteName The palette to render
* @return array
*/
protected function createPaletteContentArray($paletteName)
{
// palette needs a palette name reference, otherwise it does not make sense to try rendering of it
if (empty($paletteName) || empty($this->data['processedTca']['palettes'][$paletteName]['showitem'])) {
return [];
}
$resultStructure = [];
$foundRealElement = false; // Set to true if not only line breaks were rendered
$fieldsArray = GeneralUtility::trimExplode(',', $this->data['processedTca']['palettes'][$paletteName]['showitem'], true);
foreach ($fieldsArray as $fieldString) {
$fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
$fieldName = $fieldArray['fieldName'];
if ($fieldName === '--linebreak--') {
$resultStructure[] = [
'type' => 'linebreak',
];
} else {
if (!is_array($this->data['processedTca']['columns'][$fieldName])) {
continue;
}
$options = $this->data;
$options['fieldName'] = $fieldName;
$options['renderType'] = 'singleFieldContainer';
$singleFieldContentArray = $this->nodeFactory->create($options)->render();
if (!empty($singleFieldContentArray['html'])) {
$foundRealElement = true;
$fieldLabel = '';
if (!empty($this->data['processedTca']['columns'][$fieldName]['label'])) {
$fieldLabel = $this->data['processedTca']['columns'][$fieldName]['label'];
}
$resultStructure[] = [
'type' => 'single',
'fieldName' => $fieldName,
'fieldLabel' => $fieldLabel,
'fieldHtml' => $singleFieldContentArray['html'],
];
}
$this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray, false);
}
}
if ($foundRealElement) {
return $resultStructure;
}
return [];
}
/**
* Renders inner content of single elements of a palette and wrap it as needed
*
* @param array $elementArray Array of elements
* @return string Wrapped content
*/
protected function renderInnerPaletteContent(array $elementArray)
{
// Group fields
$groupedFields = [];
$row = 0;
$lastLineWasLinebreak = true;
foreach ($elementArray['elements'] as $element) {
if ($element['type'] === 'linebreak') {
if (!$lastLineWasLinebreak) {
$row++;
$groupedFields[$row][] = $element;
$row++;
$lastLineWasLinebreak = true;
}
} else {
$lastLineWasLinebreak = false;
$groupedFields[$row][] = $element;
}
}
$result = [];
// Process fields
foreach ($groupedFields as $fields) {
$numberOfItems = count($fields);
$colWidth = (int)floor(12 / $numberOfItems);
// Column class calculation
$colClass = 'col-md-12';
$colClear = [];
if ($colWidth == 6) {
$colClass = 'col-sm-6';
$colClear = [
2 => 'visible-sm-block visible-md-block visible-lg-block',
];
} elseif ($colWidth === 4) {
$colClass = 'col-sm-4';
$colClear = [
3 => 'visible-sm-block visible-md-block visible-lg-block',
];
} elseif ($colWidth === 3) {
$colClass = 'col-sm-6 col-md-3';
$colClear = [
2 => 'visible-sm-block',
4 => 'visible-md-block visible-lg-block',
];
} elseif ($colWidth <= 2) {
$colClass = 'checkbox-column col-sm-6 col-md-3 col-lg-2';
$colClear = [
2 => 'visible-sm-block',
4 => 'visible-md-block',
6 => 'visible-lg-block'
];
}
// Render fields
for ($counter = 0; $counter < $numberOfItems; $counter++) {
$element = $fields[$counter];
if ($element['type'] === 'linebreak') {
if ($counter !== $numberOfItems) {
$result[] = '<div class="clearfix"></div>';
}
} else {
$result[] = $this->wrapSingleFieldContentWithLabelAndOuterDiv($element, [$colClass]);
// Breakpoints
if ($counter + 1 < $numberOfItems && !empty($colClear)) {
foreach ($colClear as $rowBreakAfter => $clearClass) {
if (($counter + 1) % $rowBreakAfter === 0) {
$result[] = '<div class="clearfix ' . $clearClass . '"></div>';
}
}
}
}
}
}
return implode(LF, $result);
}
/**
* Wrap content in a field set
*
* @param string $content Incoming content
* @param bool $paletteHidden TRUE if the palette is hidden
* @param string $label Given label
* @return string Wrapped content
*/
protected function fieldSetWrap($content, $paletteHidden = false, $label = '')
{
$fieldSetClass = 'form-section';
if ($paletteHidden) {
$fieldSetClass .= ' hide';
}
$result = [];
$result[] = '<fieldset class="' . $fieldSetClass . '">';
if (!empty($label)) {
$result[] = '<h4 class="form-section-headline">' . htmlspecialchars($label) . '</h4>';
}
$result[] = $content;
$result[] = '</fieldset>';
return implode(LF, $result);
}
/**
* Wrap a single element
*
* @param array $element Given element as documented above
* @param array $additionalPaletteClasses Additional classes to be added to HTML
* @return string Wrapped element
*/
protected function wrapSingleFieldContentWithLabelAndOuterDiv(array $element, array $additionalPaletteClasses = [])
{
$fieldName = $element['fieldName'];
$paletteFieldClasses = [
'form-group',
't3js-formengine-validation-marker',
't3js-formengine-palette-field',
];
foreach ($additionalPaletteClasses as $class) {
$paletteFieldClasses[] = $class;
}
$label = BackendUtility::wrapInHelp($this->data['tableName'], $fieldName, htmlspecialchars($element['fieldLabel']));
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] && $this->getBackendUser()->isAdmin()) {
$label .= '<code>[' . htmlspecialchars($fieldName) . ']</code>';
}
$content = [];
$content[] = '<div class="' . implode(' ', $paletteFieldClasses) . '">';
$content[] = '<label class="t3js-formengine-label">';
$content[] = $label;
$content[] = '</label>';
$content[] = $element['fieldHtml'];
$content[] = '</div>';
return implode(LF, $content);
}
/**
* TRUE if field is of type user and to wrapping is requested
*
* @param array $element Current element from "target structure" array
* @return bool TRUE if user and noTableWrapping is set
*/
protected function isUserNoTableWrappingField($element)
{
$fieldName = $element['fieldName'];
if (
$this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'user'
&& !empty($this->data['processedTca']['columns'][$fieldName]['config']['noTableWrapping'])
) {
return true;
}
return false;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
/**
* Returns the current BE user.
*
* @return BackendUserAuthentication
*/
protected function getBackendUser()
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,373 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Container around a "single field".
*
* This container is the last one in the chain before processing is handed over to single element classes.
* If a single field is of type flex or inline, it however creates FlexFormEntryContainer or InlineControlContainer.
*
* The container does various checks and processing for a given single fields.
*/
class SingleFieldContainer extends AbstractContainer
{
/**
* Entry method
*
* @throws \InvalidArgumentException
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$backendUser = $this->getBackendUserAuthentication();
$resultArray = $this->initializeResultArray();
$table = $this->data['tableName'];
$row = $this->data['databaseRow'];
$fieldName = $this->data['fieldName'];
$parameterArray = [];
$parameterArray['fieldConf'] = $this->data['processedTca']['columns'][$fieldName];
$isOverlay = false;
// This field decides whether the current record is an overlay (as opposed to being a standalone record)
// Based on this decision we need to trigger field exclusion or special rendering (like readOnly)
if (isset($this->data['processedTca']['ctrl']['transOrigPointerField'])
&& is_array($this->data['processedTca']['columns'][$this->data['processedTca']['ctrl']['transOrigPointerField']])
) {
$parentValue = $row[$this->data['processedTca']['ctrl']['transOrigPointerField']];
if (MathUtility::canBeInterpretedAsInteger($parentValue)) {
$isOverlay = (bool)$parentValue;
} elseif (is_array($parentValue)) {
// This case may apply if the value has been converted to an array by the select or group data provider
$isOverlay = !empty($parentValue) ? (bool)$parentValue[0] : false;
} else {
throw new \InvalidArgumentException(
'The given value "' . $parentValue . '" for the original language field ' . $this->data['processedTca']['ctrl']['transOrigPointerField']
. ' of table ' . $table . ' is invalid.',
1470742770
);
}
}
// A couple of early returns in case the field should not be rendered
// Check if this field is configured and editable according to exclude fields and other configuration
if (// Return if BE-user has no access rights to this field, @todo: another user access rights check!
$parameterArray['fieldConf']['exclude'] && !$backendUser->check('non_exclude_fields', $table . ':' . $fieldName)
// Return if field should not be rendered in translated records
|| $isOverlay && empty($parameterArray['fieldConf']['l10n_display']) && $parameterArray['fieldConf']['l10n_mode'] === 'exclude'
|| $this->inlineFieldShouldBeSkipped()
) {
return $resultArray;
}
$parameterArray['fieldTSConfig'] = [];
if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
&& is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
) {
$parameterArray['fieldTSConfig'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'];
}
if ($parameterArray['fieldTSConfig']['disabled']) {
return $resultArray;
}
// Override fieldConf by fieldTSconfig:
$parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']);
$parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
$parameterArray['itemFormElID'] = 'data_' . $table . '_' . $row['uid'] . '_' . $fieldName;
$newElementBaseName = $this->data['elementBaseName'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
// The value to show in the form field.
$parameterArray['itemFormElValue'] = $row[$fieldName];
// Set field to read-only if configured for translated records to show default language content as readonly
if ($parameterArray['fieldConf']['l10n_display']
&& GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
&& $isOverlay
) {
$parameterArray['fieldConf']['config']['readOnly'] = true;
$parameterArray['itemFormElValue'] = $this->data['defaultLanguageRow'][$fieldName];
}
if (strpos($this->data['processedTca']['ctrl']['type'], ':') === false) {
$typeField = $this->data['processedTca']['ctrl']['type'];
} else {
$typeField = substr($this->data['processedTca']['ctrl']['type'], 0, strpos($this->data['processedTca']['ctrl']['type'], ':'));
}
// Create a JavaScript code line which will ask the user to save/update the form due to changing the element.
// This is used for eg. "type" fields and others configured with "onChange"
if (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField
|| isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload'
) {
if ($backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
$alertMsgOnChange = 'top.TYPO3.Modal.confirm('
. 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
. ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
. ')'
. '.on('
. '"button.clicked",'
. ' function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); }'
. ');';
} else {
$alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
}
} else {
$alertMsgOnChange = '';
}
// JavaScript code for event handlers:
$parameterArray['fieldChangeFunc'] = [];
$parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged('
. GeneralUtility::quoteJSvalue($table) . ','
. GeneralUtility::quoteJSvalue($row['uid']) . ','
. GeneralUtility::quoteJSvalue($fieldName) . ','
. GeneralUtility::quoteJSvalue($parameterArray['itemFormElName'])
. ');';
if ($alertMsgOnChange) {
$parameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
}
// If this is the child of an inline type and it is the field creating the label
if ($this->isInlineChildAndLabelField($table, $fieldName)) {
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
$inlineDomObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
$inlineObjectId = implode(
'-',
[
$inlineDomObjectId,
$table,
$row['uid']
]
);
$parameterArray['fieldChangeFunc']['inline'] = 'inline.handleChangedField('
. GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ','
. GeneralUtility::quoteJSvalue($inlineObjectId)
. ');';
}
// Based on the type of the item, call a render function on a child element
$options = $this->data;
$options['parameterArray'] = $parameterArray;
$options['elementBaseName'] = $newElementBaseName;
if (!empty($parameterArray['fieldConf']['config']['renderType'])) {
$options['renderType'] = $parameterArray['fieldConf']['config']['renderType'];
} else {
// Fallback to type if no renderType is given
$options['renderType'] = $parameterArray['fieldConf']['config']['type'];
}
$resultArray = $this->nodeFactory->create($options)->render();
return $resultArray;
}
/**
* Checks if the $table is the child of an inline type AND the $field is the label field of this table.
* This function is used to dynamically update the label while editing. This has no effect on labels,
* that were processed by a FormEngine-hook on saving.
*
* @param string $table The table to check
* @param string $field The field on this table to check
* @return bool Is inline child and field is responsible for the label
*/
protected function isInlineChildAndLabelField($table, $field)
{
/** @var InlineStackProcessor $inlineStackProcessor */
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
$level = $inlineStackProcessor->getStructureLevel(-1);
if ($level['config']['foreign_label']) {
$label = $level['config']['foreign_label'];
} else {
$label = $this->data['processedTca']['ctrl']['label'];
}
return $level['config']['foreign_table'] === $table && $label === $field;
}
/**
* Rendering of inline fields should be skipped under certain circumstances
*
* @return bool TRUE if field should be skipped based on inline configuration
*/
protected function inlineFieldShouldBeSkipped()
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$fieldConfig = $this->data['processedTca']['columns'][$fieldName]['config'];
/** @var InlineStackProcessor $inlineStackProcessor */
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
$structureDepth = $inlineStackProcessor->getStructureDepth();
$skipThisField = false;
if ($structureDepth > 0) {
$searchArray = [
'%OR' => [
'config' => [
0 => [
'%AND' => [
'foreign_table' => $table,
'%OR' => [
'%AND' => [
'appearance' => ['useCombination' => true],
'foreign_selector' => $fieldName
],
'MM' => $fieldConfig['MM']
]
]
],
1 => [
'%AND' => [
'foreign_table' => $fieldConfig['foreign_table'],
'foreign_selector' => $fieldConfig['foreign_field']
]
]
]
]
];
// Get the parent record from structure stack
$level = $inlineStackProcessor->getStructureLevel(-1);
// If we have symmetric fields, check on which side we are and hide fields, that are set automatically:
if ($this->data['isOnSymmetricSide']) {
$searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_field'] = $fieldName;
$searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_sortby'] = $fieldName;
} else {
$searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_field'] = $fieldName;
$searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_sortby'] = $fieldName;
}
$skipThisField = $this->arrayCompareComplex($level, $searchArray);
}
return $skipThisField;
}
/**
* Handles complex comparison requests on an array.
* A request could look like the following:
*
* $searchArray = array(
* '%AND' => array(
* 'key1' => 'value1',
* 'key2' => 'value2',
* '%OR' => array(
* 'subarray' => array(
* 'subkey' => 'subvalue'
* ),
* 'key3' => 'value3',
* 'key4' => 'value4'
* )
* )
* );
*
* It is possible to use the array keys '%AND.1', '%AND.2', etc. to prevent
* overwriting the sub-array. It could be necessary, if you use complex comparisons.
*
* The example above means, key1 *AND* key2 (and their values) have to match with
* the $subjectArray and additional one *OR* key3 or key4 have to meet the same
* condition.
* It is also possible to compare parts of a sub-array (e.g. "subarray"), so this
* function recurses down one level in that sub-array.
*
* @param array $subjectArray The array to search in
* @param array $searchArray The array with keys and values to search for
* @param string $type Use '%AND' or '%OR' for comparison
* @return bool The result of the comparison
*/
protected function arrayCompareComplex($subjectArray, $searchArray, $type = '')
{
$localMatches = 0;
$localEntries = 0;
if (is_array($searchArray) && !empty($searchArray)) {
// If no type was passed, try to determine
if (!$type) {
reset($searchArray);
$type = key($searchArray);
$searchArray = current($searchArray);
}
// We use '%AND' and '%OR' in uppercase
$type = strtoupper($type);
// Split regular elements from sub elements
foreach ($searchArray as $key => $value) {
$localEntries++;
// Process a sub-group of OR-conditions
if ($key === '%OR') {
$localMatches += $this->arrayCompareComplex($subjectArray, $value, '%OR') ? 1 : 0;
} elseif ($key === '%AND') {
$localMatches += $this->arrayCompareComplex($subjectArray, $value, '%AND') ? 1 : 0;
} elseif (is_array($value) && $this->isAssociativeArray($searchArray)) {
$localMatches += $this->arrayCompareComplex($subjectArray[$key], $value, $type) ? 1 : 0;
} elseif (is_array($value)) {
$localMatches += $this->arrayCompareComplex($subjectArray, $value, $type) ? 1 : 0;
} else {
if (isset($subjectArray[$key]) && isset($value)) {
// Boolean match:
if (is_bool($value)) {
$localMatches += !($subjectArray[$key] xor $value) ? 1 : 0;
} elseif (is_numeric($subjectArray[$key]) && is_numeric($value)) {
$localMatches += $subjectArray[$key] == $value ? 1 : 0;
} else {
$localMatches += $subjectArray[$key] === $value ? 1 : 0;
}
}
}
// If one or more matches are required ('OR'), return TRUE after the first successful match
if ($type === '%OR' && $localMatches > 0) {
return true;
}
// If all matches are required ('AND') and we have no result after the first run, return FALSE
if ($type === '%AND' && $localMatches == 0) {
return false;
}
}
}
// Return the result for '%AND' (if nothing was checked, TRUE is returned)
return $localEntries === $localMatches;
}
/**
* Checks whether an object is an associative array.
*
* @param mixed $object The object to be checked
* @return bool Returns TRUE, if the object is an associative array
*/
protected function isAssociativeArray($object)
{
return is_array($object) && !empty($object) && array_keys($object) !== range(0, count($object) - 1);
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,108 @@
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Render all tabs of a record that has tabs.
*
* This container is called from FullRecordContainer and resolves the --div-- structure,
* operates on given fieldArrays and calls a PaletteAndSingleContainer for each single tab.
*/
class TabsContainer extends AbstractContainer
{
/**
* Entry method
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws \RuntimeException
*/
public function render()
{
$languageService = $this->getLanguageService();
// All the fields to handle in a flat list
$fieldsArray = $this->data['fieldsArray'];
// Create a nested array from flat fieldArray list
$tabsArray = [];
// First element will be a --div--, so it is safe to start -1 here to trigger 0 as first array index
$currentTabIndex = -1;
foreach ($fieldsArray as $fieldString) {
$fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
if ($fieldArray['fieldName'] === '--div--') {
$currentTabIndex++;
if (empty($fieldArray['fieldLabel'])) {
throw new \RuntimeException(
'A --div-- has no label (--div--;fieldLabel) in showitem of ' . implode(',', $fieldsArray),
1426454001
);
}
$tabsArray[$currentTabIndex] = [
'label' => $languageService->sL($fieldArray['fieldLabel']),
'elements' => [],
];
} else {
$tabsArray[$currentTabIndex]['elements'][] = $fieldArray;
}
}
$resultArray = $this->initializeResultArray();
$resultArray['requireJsModules'][] = 'TYPO3/CMS/Backend/Tabs';
$domIdPrefix = 'DTM-' . GeneralUtility::shortMD5($this->data['tableName'] . $this->data['databaseRow']['uid']);
$tabCounter = 0;
$tabElements = [];
foreach ($tabsArray as $tabWithLabelAndElements) {
$tabCounter++;
$elements = $tabWithLabelAndElements['elements'];
// Merge elements of this tab into a single list again and hand over to
// palette and single field container to render this group
$options = $this->data;
$options['tabAndInlineStack'][] = [
'tab',
$domIdPrefix . '-' . $tabCounter,
];
$options['fieldsArray'] = [];
foreach ($elements as $element) {
$options['fieldsArray'][] = implode(';', $element);
}
$options['renderType'] = 'paletteAndSingleContainer';
$childArray = $this->nodeFactory->create($options)->render();
if ($childArray['html'] !== '') {
$tabElements[] = [
'label' => $tabWithLabelAndElements['label'],
'content' => $childArray['html'],
];
}
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
}
$resultArray['html'] = $this->renderTabMenu($tabElements, $domIdPrefix);
return $resultArray;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,407 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Base class for form elements of FormEngine. Contains several helper methods used by single elements.
*/
abstract class AbstractFormElement extends AbstractNode
{
/**
* Default width value for a couple of elements like text
*
* @var int
*/
protected $defaultInputWidth = 30;
/**
* Minimum width value for a couple of elements like text
*
* @var int
*/
protected $minimumInputWidth = 10;
/**
* Maximum width value for a couple of elements like text
*
* @var int
*/
protected $maxInputWidth = 50;
/**
* @var IconFactory
*/
protected $iconFactory;
/**
* Container objects give $nodeFactory down to other containers.
*
* @param NodeFactory $nodeFactory
* @param array $data
*/
public function __construct(NodeFactory $nodeFactory, array $data)
{
parent::__construct($nodeFactory, $data);
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
}
/**
* Merge field information configuration with default and render them.
*
* @return array Result array
*/
protected function renderFieldInformation(): array
{
$options = $this->data;
$fieldInformation = $this->defaultFieldInformation;
$fieldInformationFromTca = $options['parameterArray']['fieldConf']['config']['fieldInformation'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldInformation, $fieldInformationFromTca);
$options['renderType'] = 'fieldInformation';
$options['renderData']['fieldInformation'] = $fieldInformation;
return $this->nodeFactory->create($options)->render();
}
/**
* Merge field control configuration with default controls and render them.
*
* @return array Result array
*/
protected function renderFieldControl(): array
{
$options = $this->data;
$fieldControl = $this->defaultFieldControl;
$fieldControlFromTca = $options['parameterArray']['fieldConf']['config']['fieldControl'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca);
$options['renderType'] = 'fieldControl';
$options['renderData']['fieldControl'] = $fieldControl;
return $this->nodeFactory->create($options)->render();
}
/**
* Merge field wizard configuration with default wizards and render them.
*
* @return array Result array
*/
protected function renderFieldWizard(): array
{
$options = $this->data;
$fieldWizard = $this->defaultFieldWizard;
$fieldWizardFromTca = $options['parameterArray']['fieldConf']['config']['fieldWizard'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldWizard, $fieldWizardFromTca);
$options['renderType'] = 'fieldWizard';
$options['renderData']['fieldWizard'] = $fieldWizard;
return $this->nodeFactory->create($options)->render();
}
/**
* Returns true if the "null value" checkbox should be rendered. This is used in some
* "text" based types like "text" and "input" for some renderType's.
*
* A field has eval=null set, but has no useOverridePlaceholder defined.
* Goal is to have a field that can distinct between NULL and empty string in the database.
* A checkbox and an additional hidden field will be created, both with the same name
* and prefixed with "control[active]". If the checkbox is set (value 1), the value from the casual
* input field will be written to the database. If the checkbox is not set, the hidden field
* transfers value=0 to DataHandler, the value of the input field will then be reset to NULL by the
* DataHandler at an early point in processing, so NULL will be written to DB as field value.
*
* All that only works if the field is not within flex form scope since flex forms
* can not store a "null" value or distinct it from "empty string".
*
* @return bool
*/
protected function hasNullCheckboxButNoPlaceholder(): bool
{
$hasNullCheckboxNoPlaceholder = false;
$parameterArray = $this->data['parameterArray'];
$mode = $parameterArray['fieldConf']['config']['mode'] ?? '';
if (empty($this->data['flexFormDataStructureIdentifier'])
&& !empty($parameterArray['fieldConf']['config']['eval'])
&& GeneralUtility::inList($parameterArray['fieldConf']['config']['eval'], 'null')
&& ($mode !== 'useOrOverridePlaceholder')
) {
$hasNullCheckboxNoPlaceholder = true;
}
return $hasNullCheckboxNoPlaceholder;
}
/**
* Returns true if the "null value" checkbox should be rendered and the placeholder
* handling is enabled. This is used in some "text" based types like "text" and
* "input" for some renderType's.
*
* A field has useOverridePlaceholder set and null in eval and is not within a flex form.
* Here, a value from a deeper DB structure can be "fetched up" as value, and can also be overridden by a
* local value. This is used in FAL, where eg. the "title" field can have the default value from sys_file_metadata,
* the title field of sys_file_reference is then set to NULL. Or the "override" checkbox is set, and a string
* or an empty string is then written to the field of sys_file_reference.
* The situation is similar to hasNullCheckboxButNoPlaceholder(), but additionally a "default" value should be shown.
* To achieve this, again a hidden control[hidden] field is added together with a checkbox with the same name
* to transfer the information whether the default value should be used or not: Checkbox checked transfers 1 as
* value in control[active], meaning the overridden value should be used.
* Additionally to the casual input field, a second field is added containing the "placeholder" value. This
* field has no name attribute and is not transferred at all. Those two are then hidden / shown depending
* on the state of the above checkbox in via JS.
*
* @return bool
*/
protected function hasNullCheckboxWithPlaceholder(): bool
{
$hasNullCheckboxWithPlaceholder = false;
$parameterArray = $this->data['parameterArray'];
$mode = $parameterArray['fieldConf']['config']['mode'] ?? '';
if (empty($this->data['flexFormDataStructureIdentifier'])
&& !empty($parameterArray['fieldConf']['config']['eval'])
&& GeneralUtility::inList($parameterArray['fieldConf']['config']['eval'], 'null')
&& ($mode === 'useOrOverridePlaceholder')
) {
$hasNullCheckboxWithPlaceholder = true;
}
return $hasNullCheckboxWithPlaceholder;
}
/**
* Format field content if 'format' is set to date, filesize, ..., user
*
* @param string $format Configuration for the display.
* @param string $itemValue The value to display
* @param array $formatOptions Format options
* @return string Formatted field value
*/
protected function formatValue($format, $itemValue, $formatOptions = [])
{
switch ($format) {
case 'date':
if ($itemValue) {
$option = isset($formatOptions['option']) ? trim($formatOptions['option']) : '';
if ($option) {
if (isset($formatOptions['strftime']) && $formatOptions['strftime']) {
$value = strftime($option, $itemValue);
} else {
$value = date($option, $itemValue);
}
} else {
$value = date('d-m-Y', $itemValue);
}
} else {
$value = '';
}
if (isset($formatOptions['appendAge']) && $formatOptions['appendAge']) {
$age = BackendUtility::calcAge(
$GLOBALS['EXEC_TIME'] - $itemValue,
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
);
$value .= ' (' . $age . ')';
}
$itemValue = $value;
break;
case 'datetime':
// compatibility with "eval" (type "input")
if ($itemValue !== '' && $itemValue !== null) {
$itemValue = BackendUtility::datetime((int)$itemValue);
}
break;
case 'time':
// compatibility with "eval" (type "input")
if ($itemValue !== '' && $itemValue !== null) {
$itemValue = BackendUtility::time((int)$itemValue, false);
}
break;
case 'timesec':
// compatibility with "eval" (type "input")
if ($itemValue !== '' && $itemValue !== null) {
$itemValue = BackendUtility::time((int)$itemValue);
}
break;
case 'year':
// compatibility with "eval" (type "input")
if ($itemValue !== '' && $itemValue !== null) {
$itemValue = date('Y', (int)$itemValue);
}
break;
case 'int':
$baseArr = ['dec' => 'd', 'hex' => 'x', 'HEX' => 'X', 'oct' => 'o', 'bin' => 'b'];
$base = isset($formatOptions['base']) ? trim($formatOptions['base']) : '';
$format = $baseArr[$base] ?? 'd';
$itemValue = sprintf('%' . $format, $itemValue);
break;
case 'float':
// default precision
$precision = 2;
if (isset($formatOptions['precision'])) {
$precision = MathUtility::forceIntegerInRange($formatOptions['precision'], 1, 10, $precision);
}
$itemValue = sprintf('%.' . $precision . 'f', $itemValue);
break;
case 'number':
$format = isset($formatOptions['option']) ? trim($formatOptions['option']) : '';
$itemValue = sprintf('%' . $format, $itemValue);
break;
case 'md5':
$itemValue = md5($itemValue);
break;
case 'filesize':
// We need to cast to int here, otherwise empty values result in empty output,
// but we expect zero.
$value = GeneralUtility::formatSize((int)$itemValue);
if (!empty($formatOptions['appendByteSize'])) {
$value .= ' (' . $itemValue . ')';
}
$itemValue = $value;
break;
case 'user':
$func = trim($formatOptions['userFunc']);
if ($func) {
$params = [
'value' => $itemValue,
'args' => $formatOptions['userFunc'],
'config' => [
'type' => 'none',
'format' => $format,
'format.' => $formatOptions,
],
];
$itemValue = GeneralUtility::callUserFunction($func, $params, $this);
}
break;
default:
// Do nothing e.g. when $format === ''
}
return $itemValue;
}
/**
* Returns the max width in pixels for an elements like input and text
*
* @param int $size The abstract size value (1-48)
* @return int Maximum width in pixels
*/
protected function formMaxWidth($size = 48)
{
$compensationForLargeDocuments = 1.33;
$compensationForFormFields = 12;
$size = round($size * $compensationForLargeDocuments);
return ceil($size * $compensationForFormFields);
}
/***********************************************
* CheckboxElement related methods
***********************************************/
/**
* Creates checkbox parameters
*
* @param string $itemName Form element name
* @param int $formElementValue The value of the checkbox (representing checkboxes with the bits)
* @param int $checkbox Checkbox # (0-9?)
* @param int $checkboxesCount Total number of checkboxes in the array.
* @param string $additionalJavaScript Additional JavaScript for the onclick handler.
* @return string The onclick attribute + possibly the checked-option set.
* @internal
*/
protected function checkBoxParams($itemName, $formElementValue, $checkbox, $checkboxesCount, $additionalJavaScript = ''): string
{
$elementName = 'document.editform[' . GeneralUtility::quoteJSvalue($itemName) . ']';
$checkboxPow = 2 ** $checkbox;
$onClick = $elementName . '.value=this.checked?(' . $elementName . '.value|' . $checkboxPow . '):('
. $elementName . '.value&' . ((2 ** $checkboxesCount) - 1 - $checkboxPow) . ');' . $additionalJavaScript;
return ' onclick="' . htmlspecialchars($onClick) . '"' . ($formElementValue & $checkboxPow ? ' checked="checked"' : '');
}
/**
* Calculates the bootstrap grid classes based on the amount of columns
* defined in the checkbox item TCA
*
* @param $cols
* @return array
* @internal
*/
protected function calculateColumnMarkup(int $cols): array
{
$colWidth = (int)floor(12 / $cols);
$colClass = 'col-md-12';
$colClear = [];
if ($colWidth === 6) {
$colClass = 'col-sm-6';
$colClear = [
2 => 'visible-sm-block visible-md-block visible-lg-block',
];
} elseif ($colWidth === 4) {
$colClass = 'col-sm-4';
$colClear = [
3 => 'visible-sm-block visible-md-block visible-lg-block',
];
} elseif ($colWidth === 3) {
$colClass = 'col-sm-6 col-md-3';
$colClear = [
2 => 'visible-sm-block',
4 => 'visible-md-block visible-lg-block',
];
} elseif ($colWidth <= 2) {
$colClass = 'checkbox-column col-sm-6 col-md-3 col-lg-2';
$colClear = [
2 => 'visible-sm-block',
4 => 'visible-md-block',
6 => 'visible-lg-block'
];
}
return [$colClass, $colClear];
}
/**
* Append the value of a form field to its label
*
* @param string|int $label The label which can also be an integer
* @param string|int $value The value which can also be an integer
* @return string|int
*/
protected function appendValueToLabelInDebugMode($label, $value)
{
if ($value !== '' && $GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] && $this->getBackendUser()->isAdmin()) {
return $label . ' [' . $value . ']';
}
return $label;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
/**
* Returns the current BE user.
*
* @return BackendUserAuthentication
*/
protected function getBackendUser()
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,217 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of TCEform elements of the type "check"
*/
class CheckboxElement extends AbstractFormElement
{
/**
* @var IconRegistry
*/
private $iconRegistry;
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* @param NodeFactory $nodeFactory
* @param array $data
*/
public function __construct(NodeFactory $nodeFactory, array $data)
{
parent::__construct($nodeFactory, $data);
$this->iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
}
/**
* This will render a checkbox or an array of checkboxes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$elementHtml = '';
$disabled = false;
if ($this->data['parameterArray']['fieldConf']['config']['readOnly']) {
$disabled = true;
}
// Traversing the array of items
$items = $this->data['parameterArray']['fieldConf']['config']['items'];
$numberOfItems = \count($items);
if ($numberOfItems === 0) {
$items[] = ['', ''];
$numberOfItems = 1;
}
$formElementValue = (int)$this->data['parameterArray']['itemFormElValue'];
$cols = (int)$this->data['parameterArray']['fieldConf']['config']['cols'];
if ($cols > 1) {
[$colClass, $colClear] = $this->calculateColumnMarkup($cols);
$elementHtml .= '<div class="checkbox-row row">';
$counter = 0;
// $itemKey is important here, because items could have been removed via TSConfig
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition[0];
$elementHtml .=
'<div class="checkbox-column ' . $colClass . '">'
. $this->renderSingleCheckboxElement($label, $itemKey, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled) .
'</div>';
++$counter;
if ($counter < $numberOfItems && !empty($colClear)) {
foreach ($colClear as $rowBreakAfter => $clearClass) {
if ($counter % $rowBreakAfter === 0) {
$elementHtml .= '<div class="clearfix ' . $clearClass . '"></div>';
}
}
}
}
$elementHtml .= '</div>';
} else {
$counter = 0;
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition[0];
$elementHtml .= $this->renderSingleCheckboxElement($label, $counter, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled);
++$counter;
}
}
if (!$disabled) {
$elementHtml .= '<input type="hidden" name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '" value="' . htmlspecialchars((string)$formElementValue) . '" />';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = $elementHtml;
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* This functions builds the HTML output for the checkbox
*
* @param string $label Label of this item
* @param int $itemCounter Number of this element in the list of all elements
* @param int $formElementValue Value of this element
* @param int $numberOfItems Full number of items
* @param array $additionalInformation Information with additional configuration options.
* @param bool $disabled TRUE if form element is disabled
* @return string Single element HTML
*/
protected function renderSingleCheckboxElement($label, $itemCounter, $formElementValue, $numberOfItems, $additionalInformation, $disabled): string
{
$config = $additionalInformation['fieldConf']['config'];
$inline = !empty($config['cols']) && $config['cols'] === 'inline';
$invert = isset($config['items'][$itemCounter]['invertStateDisplay']) && $config['items'][$itemCounter]['invertStateDisplay'] === true;
$checkboxParameters = $this->checkBoxParams(
$additionalInformation['itemFormElName'],
$formElementValue,
$itemCounter,
$numberOfItems,
implode('', $additionalInformation['fieldChangeFunc'])
);
$uniqueId = StringUtility::getUniqueId('_');
$checkboxId = $additionalInformation['itemFormElID'] . '_' . $itemCounter . $uniqueId;
$iconIdentifierChecked = !empty($config['items'][$itemCounter]['iconIdentifierChecked']) ? $config['items'][$itemCounter]['iconIdentifierChecked'] : 'actions-check';
if (!$this->iconRegistry->isRegistered($iconIdentifierChecked)) {
$iconIdentifierChecked = 'actions-check';
}
$iconIdentifierUnchecked = !empty($config['items'][$itemCounter]['iconIdentifierUnchecked']) ? $config['items'][$itemCounter]['iconIdentifierUnchecked'] : 'empty-empty';
if (!$this->iconRegistry->isRegistered($iconIdentifierUnchecked)) {
$iconIdentifierUnchecked = 'empty-empty';
}
$iconChecked = $this->iconFactory->getIcon($iconIdentifierChecked, Icon::SIZE_SMALL)->render('inline');
$iconUnchecked = $this->iconFactory->getIcon($iconIdentifierUnchecked, Icon::SIZE_SMALL)->render('inline');
return '
<div class="checkbox checkbox-type-icon-toggle' . ($invert ? ' checkbox-invert' : '') . ($inline ? ' checkbox-inline' : '') . (!$disabled ? '' : ' disabled') . '">
<input type="checkbox"
class="checkbox-input"
value="1"
data-formengine-input-name="' . htmlspecialchars($additionalInformation['itemFormElName']) . '"
' . $checkboxParameters . '
' . ($disabled ? ' disabled="disabled"' : '') . '
id="' . $checkboxId . '" />
<label class="checkbox-label" for="' . $checkboxId . '">
<span class="checkbox-label-icon">
<span class="checkbox-label-icon-checked">' . ($invert ? $iconUnchecked : $iconChecked) . '</span>
<span class="checkbox-label-icon-unchecked">' . ($invert ? $iconChecked : $iconUnchecked) . '</span>
</span>
<span class="checkbox-label-text">' . $this->appendValueToLabelInDebugMode(($label ? htmlspecialchars($label) : '&nbsp;'), $formElementValue) . '</span>
</label>
</div>';
}
}
@@ -0,0 +1,208 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of TCEform elements of the type "check"
*/
class CheckboxLabeledToggleElement extends AbstractFormElement
{
/**
* @var IconRegistry
*/
private $iconRegistry;
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* @param NodeFactory $nodeFactory
* @param array $data
*/
public function __construct(NodeFactory $nodeFactory, array $data)
{
parent::__construct($nodeFactory, $data);
$this->iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
}
/**
* This will render a checkbox or an array of checkboxes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$elementHtml = '';
$disabled = false;
if ($this->data['parameterArray']['fieldConf']['config']['readOnly']) {
$disabled = true;
}
// Traversing the array of items
$items = $this->data['parameterArray']['fieldConf']['config']['items'];
$numberOfItems = \count($items);
if ($numberOfItems === 0) {
$items[] = ['', ''];
$numberOfItems = 1;
}
$formElementValue = (int)$this->data['parameterArray']['itemFormElValue'];
$cols = (int)$this->data['parameterArray']['fieldConf']['config']['cols'];
if ($cols > 1) {
[$colClass, $colClear] = $this->calculateColumnMarkup($cols);
$elementHtml .= '<div class="checkbox-row row">';
$counter = 0;
// $itemKey is important here, because items could have been removed via TSConfig
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition[0];
$elementHtml .=
'<div class="checkbox-column ' . $colClass . '">'
. $this->renderSingleCheckboxElement($label, $itemKey, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled) .
'</div>';
++$counter;
if ($counter < $numberOfItems && !empty($colClear)) {
foreach ($colClear as $rowBreakAfter => $clearClass) {
if ($counter % $rowBreakAfter === 0) {
$elementHtml .= '<div class="clearfix ' . $clearClass . '"></div>';
}
}
}
}
$elementHtml .= '</div>';
} else {
$counter = 0;
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition[0];
$elementHtml .= $this->renderSingleCheckboxElement($label, $counter, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled);
++$counter;
}
}
if (!$disabled) {
$elementHtml .= '<input type="hidden" name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '" value="' . htmlspecialchars((string)$formElementValue) . '" />';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = $elementHtml;
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* This functions builds the HTML output for the checkbox
*
* @param string $label Label of this item
* @param int $itemCounter Number of this element in the list of all elements
* @param int $formElementValue Value of this element
* @param int $numberOfItems Full number of items
* @param array $additionalInformation Information with additional configuration options.
* @param bool $disabled TRUE if form element is disabled
* @return string Single element HTML
*/
protected function renderSingleCheckboxElement($label, $itemCounter, $formElementValue, $numberOfItems, $additionalInformation, $disabled): string
{
$config = $additionalInformation['fieldConf']['config'];
$inline = !empty($config['cols']) && $config['cols'] === 'inline';
$invert = isset($config['items'][$itemCounter]['invertStateDisplay']) && $config['items'][$itemCounter]['invertStateDisplay'] === true;
$checkboxParameters = $this->checkBoxParams(
$additionalInformation['itemFormElName'],
$formElementValue,
$itemCounter,
$numberOfItems,
implode('', $additionalInformation['fieldChangeFunc'])
);
$uniqueId = StringUtility::getUniqueId('_');
$checkboxId = $additionalInformation['itemFormElID'] . '_' . $itemCounter . $uniqueId;
return '
<div class="checkbox checkbox-type-labeled-toggle' . ($invert ? ' checkbox-invert' : '') . ($inline ? ' checkbox-inline' : '') . (!$disabled ? '' : ' disabled') . '">
<input type="checkbox"
class="checkbox-input"
value="1"
data-formengine-input-name="' . htmlspecialchars($additionalInformation['itemFormElName']) . '"
' . $checkboxParameters . '
' . (!$disabled ?: ' disabled="disabled"') . '
id="' . $checkboxId . '" />
<label class="checkbox-label" for="' . $checkboxId . '">
<span class="checkbox-label-switch">
<span class="checkbox-label-switch-checked">
' . $config['items'][$itemCounter]['labelChecked'] . '
</span>
<span class="checkbox-label-switch-unchecked">
' . $config['items'][$itemCounter]['labelUnchecked'] . '
</span>
</span>
<span class="checkbox-label-text">' . $this->appendValueToLabelInDebugMode(($label ? htmlspecialchars($label) : '&nbsp;'), $formElementValue) . '</span>
</label>
</div>';
}
}
@@ -0,0 +1,200 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of TCEform elements of the type "check"
*/
class CheckboxToggleElement extends AbstractFormElement
{
/**
* @var IconRegistry
*/
private $iconRegistry;
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* @param NodeFactory $nodeFactory
* @param array $data
*/
public function __construct(NodeFactory $nodeFactory, array $data)
{
parent::__construct($nodeFactory, $data);
$this->iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
}
/**
* This will render a checkbox or an array of checkboxes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$elementHtml = '';
$disabled = false;
if ($this->data['parameterArray']['fieldConf']['config']['readOnly']) {
$disabled = true;
}
// Traversing the array of items
$items = $this->data['parameterArray']['fieldConf']['config']['items'];
$numberOfItems = count($items);
if ($numberOfItems === 0) {
$items[] = ['', ''];
$numberOfItems = 1;
}
$formElementValue = (int)$this->data['parameterArray']['itemFormElValue'];
$cols = (int)$this->data['parameterArray']['fieldConf']['config']['cols'];
if ($cols > 1) {
[$colClass, $colClear] = $this->calculateColumnMarkup($cols);
$elementHtml .= '<div class="checkbox-row row">';
$counter = 0;
// $itemKey is important here, because items could have been removed via TSConfig
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition[0];
$elementHtml .=
'<div class="checkbox-column ' . $colClass . '">'
. $this->renderSingleCheckboxElement($label, $itemKey, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled) .
'</div>';
++$counter;
if ($counter < $numberOfItems && !empty($colClear)) {
foreach ($colClear as $rowBreakAfter => $clearClass) {
if ($counter % $rowBreakAfter === 0) {
$elementHtml .= '<div class="clearfix ' . $clearClass . '"></div>';
}
}
}
}
$elementHtml .= '</div>';
} else {
$counter = 0;
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition[0];
$elementHtml .= $this->renderSingleCheckboxElement($label, $counter, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled);
++$counter;
}
}
if (!$disabled) {
$elementHtml .= '<input type="hidden" name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '" value="' . htmlspecialchars((string)$formElementValue) . '" />';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = $elementHtml;
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* This functions builds the HTML output for the checkbox
*
* @param string $label Label of this item
* @param int $itemCounter Number of this element in the list of all elements
* @param int $formElementValue Value of this element
* @param int $numberOfItems Full number of items
* @param array $additionalInformation Information with additional configuration options.
* @param bool $disabled TRUE if form element is disabled
* @return string Single element HTML
*/
protected function renderSingleCheckboxElement($label, $itemCounter, $formElementValue, $numberOfItems, $additionalInformation, $disabled): string
{
$config = $additionalInformation['fieldConf']['config'];
$inline = !empty($config['cols']) && $config['cols'] === 'inline';
$invert = isset($config['items'][0]['invertStateDisplay']) && $config['items'][0]['invertStateDisplay'] === true;
$checkboxParameters = $this->checkBoxParams(
$additionalInformation['itemFormElName'],
$formElementValue,
$itemCounter,
$numberOfItems,
implode('', $additionalInformation['fieldChangeFunc'])
);
$uniqueId = StringUtility::getUniqueId('_');
$checkboxId = $additionalInformation['itemFormElID'] . '_' . $itemCounter . $uniqueId;
return '
<div class="checkbox checkbox-type-toggle' . ($invert ? ' checkbox-invert' : '') . ($inline ? ' checkbox-inline' : '') . (!$disabled ? '' : ' disabled') . '">
<input type="checkbox"
class="checkbox-input"
value="1"
data-formengine-input-name="' . htmlspecialchars($additionalInformation['itemFormElName']) . '"
' . $checkboxParameters . '
' . (!$disabled ?: ' disabled="disabled"') . '
id="' . $checkboxId . '" />
<label class="checkbox-label" for="' . $checkboxId . '">
<span class="checkbox-label-text">' . $this->appendValueToLabelInDebugMode(($label ? htmlspecialchars($label) : '&nbsp;'), $formElementValue) . '</span>
</label>
</div>';
}
}
@@ -0,0 +1,97 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This renderType is used with type=user in FAL for table sys_file and
* sys_file_metadata, for field fileinfo and renders an informational
* element with image preview, filename, size and similar.
*/
class FileInfoElement extends AbstractFormElement
{
/**
* Handler for single nodes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$fileUid = 0;
if ($this->data['tableName'] === 'sys_file') {
$fileUid = (int)$this->data['databaseRow']['uid'];
} elseif ($this->data['tableName'] === 'sys_file_metadata') {
$fileUid = (int)$this->data['databaseRow']['file'][0];
}
$fileObject = null;
if ($fileUid > 0) {
$fileObject = ResourceFactory::getInstance()->getFileObject($fileUid);
}
$resultArray['html'] = $this->renderFileInformationContent($fileObject);
return $resultArray;
}
/**
* Renders a HTML Block with file information
*
* @param File $file
* @return string
*/
protected function renderFileInformationContent(File $file = null): string
{
/** @var LanguageService $lang */
$lang = $GLOBALS['LANG'];
if ($file !== null) {
$content = '';
if ($file->isMissing()) {
$content .= '<span class="label label-danger label-space-right">'
. htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
. '</span>';
}
if (GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $file->getExtension())) {
$processedFile = $file->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, ['width' => 150, 'height' => 150]);
$previewImage = $processedFile->getPublicUrl(true);
if ($previewImage) {
$content .= '<img src="' . htmlspecialchars($previewImage) . '" ' .
'width="' . $processedFile->getProperty('width') . '" ' .
'height="' . $processedFile->getProperty('height') . '" ' .
'alt="" class="t3-tceforms-sysfile-imagepreview" />';
}
}
$content .= '<strong>' . htmlspecialchars($file->getName()) . '</strong>';
$content .= ' (' . htmlspecialchars(GeneralUtility::formatSize($file->getSize())) . 'bytes)<br />';
$content .= BackendUtility::getProcessedValue('sys_file', 'type', $file->getType()) . ' (' . $file->getMimeType() . ')<br />';
$content .= htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:fileMetaDataLocation')) . ': ';
$content .= htmlspecialchars($file->getStorage()->getName()) . ' - ' . htmlspecialchars($file->getIdentifier()) . '<br />';
$content .= '<br />';
} else {
$content = '<h2>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:fileMetaErrorInvalidRecord')) . '</h2>';
}
return $content;
}
}
@@ -0,0 +1,425 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of elements of the type "group"
*/
class GroupElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field controls for this element.
*
* @var array
*/
protected $defaultFieldControl = [
'elementBrowser' => [
'renderType' => 'elementBrowser',
],
'insertClipboard' => [
'renderType' => 'insertClipboard',
'after' => [ 'elementBrowser' ],
],
'editPopup' => [
'renderType' => 'editPopup',
'disabled' => true,
'after' => [ 'insertClipboard' ],
],
'addRecord' => [
'renderType' => 'addRecord',
'disabled' => true,
'after' => [ 'editPopup' ],
],
'listModule' => [
'renderType' => 'listModule',
'disabled' => true,
'after' => [ 'addRecord' ],
],
];
/**
* Default field wizards for this element
*
* @var array
*/
protected $defaultFieldWizard = [
'tableList' => [
'renderType' => 'tableList',
],
'fileTypeList' => [
'renderType' => 'fileTypeList',
'after' => [ 'tableList' ],
],
'fileThumbnails' => [
'renderType' => 'fileThumbnails',
'after' => [ 'fileTypeList' ],
],
'recordsOverview' => [
'renderType' => 'recordsOverview',
'after' => [ 'fileThumbnails' ],
],
'fileUpload' => [
'renderType' => 'fileUpload',
'after' => [ 'recordsOverview' ],
],
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
'after' => [ 'fileUpload' ],
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [ 'localizationStateSelector' ],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [ 'otherLanguageContent' ],
],
];
/**
* This will render a selector box into which elements from either
* the file system or database can be inserted. Relations.
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws \RuntimeException
*/
public function render()
{
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUserAuthentication();
$resultArray = $this->initializeResultArray();
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$elementName = $parameterArray['itemFormElName'];
$selectedItems = $parameterArray['itemFormElValue'];
$selectedItemsCount = count($selectedItems);
$maxItems = $config['maxitems'];
$autoSizeMax = MathUtility::forceIntegerInRange($config['autoSizeMax'], 0);
$size = 5;
if (isset($config['size'])) {
$size = (int)$config['size'];
}
if ($autoSizeMax >= 1) {
$size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
}
$internalType = (string)$config['internal_type'];
$maxTitleLength = $backendUser->uc['titleLen'];
$listOfSelectedValues = [];
$selectorOptionsHtml = [];
if ($internalType === 'file_reference' || $internalType === 'file') {
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Deprecation logged by TcaMigration class.
foreach ($selectedItems as $selectedItem) {
$uidOrPath = $selectedItem['uidOrPath'];
$listOfSelectedValues[] = $uidOrPath;
$title = $selectedItem['title'];
$shortenedTitle = GeneralUtility::fixed_lgd_cs($title, $maxTitleLength);
$selectorOptionsHtml[] =
'<option value="' . htmlspecialchars($uidOrPath) . '" title="' . htmlspecialchars($title) . '">'
. htmlspecialchars($this->appendValueToLabelInDebugMode($shortenedTitle, $uidOrPath))
. '</option>';
}
} elseif ($internalType === 'folder') {
foreach ($selectedItems as $selectedItem) {
$folder = $selectedItem['folder'];
$listOfSelectedValues[] = $folder;
$selectorOptionsHtml[] =
'<option value="' . htmlspecialchars($folder) . '" title="' . htmlspecialchars($folder) . '">'
. htmlspecialchars($folder)
. '</option>';
}
} elseif ($internalType === 'db') {
foreach ($selectedItems as $selectedItem) {
$tableWithUid = $selectedItem['table'] . '_' . $selectedItem['uid'];
$listOfSelectedValues[] = $tableWithUid;
$title = $selectedItem['title'];
if (empty($title)) {
$title = '[' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']';
}
$shortenedTitle = GeneralUtility::fixed_lgd_cs($title, $maxTitleLength);
$selectorOptionsHtml[] =
'<option value="' . htmlspecialchars($tableWithUid) . '" title="' . htmlspecialchars($title) . '">'
. htmlspecialchars($this->appendValueToLabelInDebugMode($shortenedTitle, $tableWithUid))
. '</option>';
}
} else {
throw new \RuntimeException(
'internal_type missing on type="group" field',
1485007097
);
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if (isset($config['readOnly']) && $config['readOnly']) {
// Return early if element is read only
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<select';
$html[] = ' size="' . $size . '"';
$html[] = ' disabled="disabled"';
$html[] = ' class="form-control tceforms-multiselect"';
$html[] = ($maxItems !== 1 && $size !== 1) ? ' multiple="multiple"' : '';
$html[] = '>';
$html[] = implode(LF, $selectorOptionsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '<div class="form-wizards-items-aside">';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
// Need some information if in flex form scope for the suggest element
$dataStructureIdentifier = '';
$flexFormSheetName = '';
$flexFormFieldName = '';
$flexFormContainerName = '';
$flexFormContainerFieldName = '';
if ($this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
$flexFormConfig = $this->data['processedTca']['columns'][$fieldName];
$dataStructureIdentifier = $flexFormConfig['config']['dataStructureIdentifier'];
if (!isset($flexFormConfig['config']['dataStructureIdentifier'])) {
throw new \RuntimeException(
'A data structure identifier must be set in [\'config\'] part of a flex form.'
. ' This is usually added by TcaFlexPrepare data processor',
1485206970
);
}
if (isset($this->data['flexFormSheetName'])) {
$flexFormSheetName = $this->data['flexFormSheetName'];
}
if (isset($this->data['flexFormFieldName'])) {
$flexFormFieldName = $this->data['flexFormFieldName'];
}
if (isset($this->data['flexFormContainerName'])) {
$flexFormContainerName = $this->data['flexFormContainerName'];
}
if (isset($this->data['flexFormContainerFieldName'])) {
$flexFormContainerFieldName = $this->data['flexFormContainerFieldName'];
}
}
// Get minimum characters for suggest from TCA and override by TsConfig
$suggestMinimumCharacters = 0;
if (isset($config['suggestOptions']['default']['minimumCharacters'])) {
$suggestMinimumCharacters = (int)$config['suggestOptions']['default']['minimumCharacters'];
}
if (isset($parameterArray['fieldTSConfig']['suggest.']['default.']['minimumCharacters'])) {
$suggestMinimumCharacters = (int)$parameterArray['fieldTSConfig']['suggest.']['default.']['minimumCharacters'];
}
$suggestMinimumCharacters = $suggestMinimumCharacters > 0 ? $suggestMinimumCharacters : 2;
$itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
$showMoveIcons = true;
if (isset($config['hideMoveIcons']) && $config['hideMoveIcons']) {
$showMoveIcons = false;
}
$showDeleteControl = true;
if (isset($config['hideDeleteIcon']) && $config['hideDeleteIcon']) {
$showDeleteControl = false;
}
// Check against inline uniqueness - Create some onclick js for delete control and element browser
// to override record selection in some FAL scenarios - See 'appearance' docs of group element
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
$deleteControlOnClick = '';
if ($this->data['isInlineChild']
&& $this->data['inlineParentUid']
&& $this->data['inlineParentConfig']['foreign_table'] === $table
&& $this->data['inlineParentConfig']['foreign_unique'] === $fieldName
) {
$objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']) . '-' . $table;
$deleteControlOnClick = 'inline.revertUnique(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',null,' . GeneralUtility::quoteJSvalue($row['uid']) . ');';
}
$selectorAttributes = [
'id' => StringUtility::getUniqueId('tceforms-multiselect-'),
'data-formengine-input-name' => htmlspecialchars($elementName),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'size' => $size,
];
$selectorClasses = [
'form-control',
'tceforms-multiselect',
];
if ($maxItems === 1) {
$selectorClasses[] = 'form-select-no-siblings';
}
$selectorAttributes['class'] = implode(' ', $selectorClasses);
if ($maxItems !== 1 && $size !== 1) {
$selectorAttributes['multiple'] = 'multiple';
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
if ($internalType === 'db' && (!isset($config['hideSuggest']) || (bool)$config['hideSuggest'] !== true)) {
$html[] = '<div class="form-wizards-items-top">';
$html[] = '<div class="autocomplete t3-form-suggest-container">';
$html[] = '<div class="input-group">';
$html[] = '<span class="input-group-addon">';
$html[] = $this->iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render();
$html[] = '</span>';
$html[] = '<input type="search" class="t3-form-suggest form-control"';
$html[] = ' placeholder="' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.findRecord') . '"';
$html[] = ' data-fieldname="' . htmlspecialchars($fieldName) . '"';
$html[] = ' data-tablename="' . htmlspecialchars($table) . '"';
$html[] = ' data-field="' . htmlspecialchars($elementName) . '"';
$html[] = ' data-uid="' . htmlspecialchars($this->data['databaseRow']['uid']) . '"';
$html[] = ' data-pid="' . htmlspecialchars($this->data['parentPageRow']['uid'] ?? 0) . '"';
$html[] = ' data-fieldtype="' . htmlspecialchars($config['type']) . '"';
$html[] = ' data-minchars="' . htmlspecialchars($suggestMinimumCharacters) . '"';
$html[] = ' data-datastructureidentifier="' . htmlspecialchars($dataStructureIdentifier) . '"';
$html[] = ' data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
$html[] = ' data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
$html[] = ' data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
$html[] = ' data-flexformcontainerfieldname="' . htmlspecialchars($flexFormContainerFieldName) . '"';
$html[] = '/>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '<div class="form-wizards-element">';
$html[] = '<input type="hidden" class="t3js-group-hidden-field" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . $itemCanBeSelectedMoreThanOnce . '" />';
$html[] = '<select ' . GeneralUtility::implodeAttributes($selectorAttributes, true) . '>';
$html[] = implode(LF, $selectorOptionsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '<div class="form-wizards-items-aside">';
$html[] = '<div class="btn-group-vertical">';
if ($maxItems > 1 && $size >=5 && $showMoveIcons) {
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-moveoption-top"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-top', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
}
if ($maxItems > 1 && $size > 1 && $showMoveIcons) {
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-moveoption-up"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-moveoption-down"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
}
if ($maxItems > 1 && $size >= 5 && $showMoveIcons) {
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-moveoption-bottom"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-bottom', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
}
if ($showDeleteControl) {
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-removeoption"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
$html[] = ' onClick="' . $deleteControlOnClick . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-selection-delete', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<div class="form-wizards-items-aside">';
$html[] = '<div class="btn-group-vertical">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '<input type="hidden" name="' . htmlspecialchars($elementName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,340 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Imaging\ImageManipulation\InvalidConfigurationException;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* Generation of image manipulation FormEngine element.
* This is typically used in FAL relations to cut images.
*/
class ImageManipulationElement extends AbstractFormElement
{
/**
* @var string
*/
private $wizardRouteName = 'ajax_wizard_image_manipulation';
/**
* Default element configuration
*
* @var array
*/
protected static $defaultConfig = [
'file_field' => 'uid_local',
'allowedExtensions' => null, // default: $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
'cropVariants' => [
'default' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.crop_variant.default',
'allowedAspectRatios' => [
'16:9' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.16_9',
'value' => 16 / 9
],
'3:2' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.3_2',
'value' => 3 / 2
],
'4:3' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3',
'value' => 4 / 3
],
'1:1' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.1_1',
'value' => 1.0
],
'NaN' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free',
'value' => 0.0
],
],
'selectedRatio' => 'NaN',
'cropArea' => [
'x' => 0.0,
'y' => 0.0,
'width' => 1.0,
'height' => 1.0,
],
],
]
];
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* @var StandaloneView
*/
protected $templateView;
/**
* @var UriBuilder
*/
protected $uriBuilder;
/**
* @param NodeFactory $nodeFactory
* @param array $data
*/
public function __construct(NodeFactory $nodeFactory, array $data)
{
parent::__construct($nodeFactory, $data);
// Would be great, if we could inject the view here, but since the constructor is in the interface, we can't
$this->templateView = GeneralUtility::makeInstance(StandaloneView::class);
$this->templateView->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts/')]);
$this->templateView->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials/ImageManipulation/')]);
$this->templateView->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ImageManipulation/ImageManipulationElement.html'));
$this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
}
/**
* This will render an imageManipulation field
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws \TYPO3\CMS\Core\Imaging\ImageManipulation\InvalidConfigurationException
*/
public function render()
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $this->populateConfiguration($parameterArray['fieldConf']['config']);
$file = $this->getFile($this->data['databaseRow'], $config['file_field']);
if (!$file) {
// Early return in case we do not find a file
return $resultArray;
}
$config = $this->processConfiguration($config, $parameterArray['itemFormElValue'], $file);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$arguments = [
'fieldInformation' => $fieldInformationHtml,
'fieldControl' => $fieldControlHtml,
'fieldWizard' => $fieldWizardHtml,
'isAllowedFileExtension' => in_array(strtolower($file->getExtension()), GeneralUtility::trimExplode(',', strtolower($config['allowedExtensions'])), true),
'image' => $file,
'formEngine' => [
'field' => [
'value' => $parameterArray['itemFormElValue'],
'name' => $parameterArray['itemFormElName']
],
'validation' => '[]'
],
'config' => $config,
'wizardUri' => $this->getWizardUri(),
'wizardPayload' => json_encode($this->getWizardPayload($config['cropVariants'], $file)),
'previewUrl' => $this->getPreviewUrl($this->data['databaseRow'], $file),
];
if ($arguments['isAllowedFileExtension']) {
$resultArray['requireJsModules'][] = [
'TYPO3/CMS/Backend/ImageManipulation' => 'function (ImageManipulation) {top.require(["cropper"], function() { ImageManipulation.initializeTrigger(); }); }'
];
$arguments['formEngine']['field']['id'] = StringUtility::getUniqueId('formengine-image-manipulation-');
if (GeneralUtility::inList($config['eval'], 'required')) {
$arguments['formEngine']['validation'] = $this->getValidationDataAsJsonString(['required' => true]);
}
}
$this->templateView->assignMultiple($arguments);
$resultArray['html'] = $this->templateView->render();
return $resultArray;
}
/**
* Get file object
*
* @param array $row
* @param string $fieldName
* @return File|null
*/
protected function getFile(array $row, $fieldName)
{
$file = null;
$fileUid = !empty($row[$fieldName]) ? $row[$fieldName] : null;
if (is_array($fileUid) && isset($fileUid[0]['uid'])) {
$fileUid = $fileUid[0]['uid'];
}
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
try {
$file = ResourceFactory::getInstance()->getFileObject($fileUid);
} catch (FileDoesNotExistException $e) {
} catch (\InvalidArgumentException $e) {
}
}
return $file;
}
/**
* @param array $databaseRow
* @param File $file
* @return string
*/
protected function getPreviewUrl(array $databaseRow, File $file): string
{
$previewUrl = '';
// Hook to generate a preview URL
$hookParameters = [
'databaseRow' => $databaseRow,
'file' => $file,
'previewUrl' => $previewUrl,
];
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Backend/Form/Element/ImageManipulationElement']['previewUrl'] ?? [] as $listener) {
$previewUrl = GeneralUtility::callUserFunction($listener, $hookParameters, $this);
}
return $previewUrl;
}
/**
* @param array $baseConfiguration
* @return array
* @throws InvalidConfigurationException
*/
protected function populateConfiguration(array $baseConfiguration)
{
$defaultConfig = self::$defaultConfig;
// If ratios are set do not add default options
if (isset($baseConfiguration['cropVariants'])) {
unset($defaultConfig['cropVariants']);
}
$config = array_replace_recursive($defaultConfig, $baseConfiguration);
if (!is_array($config['cropVariants'])) {
throw new InvalidConfigurationException('Crop variants configuration must be an array', 1485377267);
}
$cropVariants = [];
foreach ($config['cropVariants'] as $id => $cropVariant) {
// Ignore disabled crop variants
if (!empty($cropVariant['disabled'])) {
continue;
}
// Enforce a crop area (default is full image)
if (empty($cropVariant['cropArea'])) {
$cropVariant['cropArea'] = Area::createEmpty()->asArray();
}
$cropVariants[$id] = $cropVariant;
}
$config['cropVariants'] = $cropVariants;
// By default we allow all image extensions that can be handled by the GFX functionality
if ($config['allowedExtensions'] === null) {
$config['allowedExtensions'] = $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'];
}
return $config;
}
/**
* @param array $config
* @param string $elementValue
* @param File $file
* @return array
* @throws \TYPO3\CMS\Core\Imaging\ImageManipulation\InvalidConfigurationException
*/
protected function processConfiguration(array $config, string &$elementValue, File $file)
{
$cropVariantCollection = CropVariantCollection::create($elementValue, $config['cropVariants']);
if (empty($config['readOnly']) && !empty($file->getProperty('width'))) {
$cropVariantCollection = $cropVariantCollection->applyRatioRestrictionToSelectedCropArea($file);
$elementValue = (string)$cropVariantCollection;
}
$config['cropVariants'] = $cropVariantCollection->asArray();
$config['allowedExtensions'] = implode(', ', GeneralUtility::trimExplode(',', $config['allowedExtensions'], true));
return $config;
}
/**
* @return string
*/
protected function getWizardUri(): string
{
return (string)$this->uriBuilder->buildUriFromRoute($this->wizardRouteName);
}
/**
* @param array $cropVariants
* @param File $image
* @return array
*/
protected function getWizardPayload(array $cropVariants, File $image): array
{
$arguments = [
'cropVariants' => $cropVariants,
'image' => $image->getUid(),
];
$uriArguments['arguments'] = json_encode($arguments);
$uriArguments['signature'] = GeneralUtility::hmac($uriArguments['arguments'], $this->wizardRouteName);
return $uriArguments;
}
}
@@ -0,0 +1,45 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
/**
* Interface for classes which hook into inline element handling
*/
interface InlineElementHookInterface
{
/**
* Pre-processing to define which control items are enabled or disabled.
*
* @param string $parentUid The uid of the parent (embedding) record (uid or NEW...)
* @param string $foreignTable The table (foreign_table) we create control-icons for
* @param array $childRecord The current record of that foreign_table
* @param array $childConfig TCA configuration of the current field of the child record
* @param bool $isVirtual Defines whether the current records is only virtually shown and not physically part of the parent record
* @param array &$enabledControls (reference) Associative array with the enabled control items
*/
public function renderForeignRecordHeaderControl_preProcess($parentUid, $foreignTable, array $childRecord, array $childConfig, $isVirtual, array &$enabledControls);
/**
* Post-processing to define which control items to show. Possibly own icons can be added here.
*
* @param string $parentUid The uid of the parent (embedding) record (uid or NEW...)
* @param string $foreignTable The table (foreign_table) we create control-icons for
* @param array $childRecord The current record of that foreign_table
* @param array $childConfig TCA configuration of the current field of the child record
* @param bool $isVirtual Defines whether the current records is only virtually shown and not physically part of the parent record
* @param array &$controlItems (reference) Associative array with the currently available control items
*/
public function renderForeignRecordHeaderControl_postProcess($parentUid, $foreignTable, array $childRecord, array $childConfig, $isVirtual, array &$controlItems);
}
@@ -0,0 +1,265 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render an input field with a color picker
*/
class InputColorPickerElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$itemValue = $parameterArray['itemFormElValue'];
$config = $parameterArray['fieldConf']['config'];
$size = MathUtility::forceIntegerInRange($config['size'] ?: $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
$width = (int)$this->formMaxWidth($size);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']');
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly']) {
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
// @todo: The whole eval handling is a mess and needs refactoring
foreach ($evalList as $func) {
// @todo: This is ugly: The code should find out on it's own whether a eval definition is a
// @todo: keyword like "date", or a class reference. The global registration could be dropped then
// Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
if (class_exists($func)) {
$evalObj = GeneralUtility::makeInstance($func);
if (method_exists($evalObj, 'deevaluateFieldValue')) {
$_params = [
'value' => $itemValue
];
$itemValue = $evalObj->deevaluateFieldValue($_params);
}
if (method_exists($evalObj, 'returnFieldJS')) {
$resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($evalData) . '] = function(value) {' . $evalObj->returnFieldJS() . '};';
}
}
}
}
// Load needed js library
$resultArray['requireJsModules'][] = [
'TYPO3/CMS/Backend/ColorPicker' => 'function(ColorPicker){ColorPicker.initialize()}'
];
$attributes = [
'value' => $itemValue,
'id' => StringUtility::getUniqueId('formengine-input-'),
'class' => implode(' ', [
'form-control',
'hasDefaultValue',
't3js-clearable',
't3js-color-picker',
'formengine-colorpickerelement',
]),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => json_encode([
'field' => $parameterArray['itemFormElName'],
'evalList' => implode(',', $evalList),
'is_in' => trim($config['is_in']),
]),
'data-formengine-input-name' => $parameterArray['itemFormElName'],
];
if (isset($config['max']) && (int)$config['max'] > 0) {
$attributes['maxlength'] = (int)$config['max'];
}
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
if (isset($config['autocomplete'])) {
$attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
}
$valuePickerHtml = [];
if (isset($config['valuePicker']['items']) && is_array($config['valuePicker']['items'])) {
$valuePickerHtml[] = '<select class="t3js-colorpicker-value-trigger form-control tceforms-select tceforms-wizardselect">';
$valuePickerHtml[] = '<option></option>';
foreach ($config['valuePicker']['items'] as $item) {
$valuePickerHtml[] = '<option value="' . htmlspecialchars($item[1]) . '">' . htmlspecialchars($languageService->sL($item[0])) . '</option>';
}
$valuePickerHtml[] = '</select>';
}
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-element">';
$mainFieldHtml[] = '<input type="text" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
$mainFieldHtml[] = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '<div class="form-wizards-items-aside">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = implode(LF, $valuePickerHtml);
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
if (!empty($fieldWizardHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-items-bottom">';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml = implode(LF, $mainFieldHtml);
$fullElement = $mainFieldHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $mainFieldHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
$disabled = '';
$fallbackValue = 0;
if (strlen($placeholder) > 0) {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="' . $fallbackValue . '" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . $disabled . ' />';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $mainFieldHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = '<div class="formengine-field-item t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
return $resultArray;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,289 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of TCEform elements of the type "input type=text"
*/
class InputDateTimeElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws \RuntimeException with invalid configuration
*/
public function render()
{
$languageService = $this->getLanguageService();
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
$itemValue = $parameterArray['itemFormElValue'];
$defaultInputWidth = 10;
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']');
if (in_array('date', $evalList, true)) {
$format = 'date';
$defaultInputWidth = 13;
} elseif (in_array('datetime', $evalList, true)) {
$format = 'datetime';
$defaultInputWidth = 13;
} elseif (in_array('time', $evalList, true)) {
$format = 'time';
} elseif (in_array('timesec', $evalList, true)) {
$format = 'timesec';
} else {
throw new \RuntimeException(
'Field "' . $fieldName . '" in table "' . $table . '" with renderType "inputDataTime" needs'
. '"eval" set to either "date", "datetime", "time" or "timesec"',
1483823746
);
}
$size = MathUtility::forceIntegerInRange($config['size'] ?? $defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
$width = (int)$this->formMaxWidth($size);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
// Early return for read only fields
if (isset($config['readOnly']) && $config['readOnly']) {
// Ensure dbType values (see DatabaseRowDateTimeFields) are converted to a UNIX timestamp before rendering read-only
if (!empty($itemValue) && !MathUtility::canBeInterpretedAsInteger($itemValue)) {
$itemValue = (new \DateTime($itemValue))->getTimestamp();
}
// Format the unix-timestamp to the defined format (date/year etc)
$itemValue = $this->formatValue($format, $itemValue);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$attributes = [
'value' => '',
'id' => StringUtility::getUniqueId('formengine-input-'),
'class' => implode(' ', [
't3js-datetimepicker',
'form-control',
't3js-clearable',
'hasDefaultValue',
]),
'data-date-type' => $format,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => json_encode([
'field' => $parameterArray['itemFormElName'],
'evalList' => implode(',', $evalList)
]),
'data-formengine-input-name' => $parameterArray['itemFormElName'],
];
$maxLength = $config['max'] ?? 0;
if ((int)$maxLength > 0) {
$attributes['maxlength'] = (int)$maxLength;
}
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
if ($format === 'datetime' || $format === 'date') {
// This only handles integer timestamps; if the field is a SQL native date(time), it was already converted
// to an ISO-8601 date by the DatabaseRowDateTimeFields class. (those dates are stored as server local time)
if (MathUtility::canBeInterpretedAsInteger($itemValue) && $itemValue != 0) {
// We store UTC timestamps in the database.
// Convert the timestamp to a proper ISO-8601 date so we get rid of timezone issues on the client.
// Details: As the JS side is not capable of handling dates in the server's timezone
// (moment.js can only handle UTC or browser's local timezone), we need to offset the value
// to eliminate the timezone. JS will receive all dates as if they were UTC, which we undo on save in DataHandler
$adjustedValue = $itemValue + date('Z', (int)$itemValue);
// output date as a ISO-8601 date
$itemValue = gmdate('c', $adjustedValue);
}
if (isset($config['range']['lower'])) {
$attributes['data-date-min-date'] = (int)$config['range']['lower'] * 1000;
}
if (isset($config['range']['upper'])) {
$attributes['data-date-max-date'] = (int)$config['range']['upper'] * 1000;
}
}
if (($format === 'time' || $format === 'timesec') && MathUtility::canBeInterpretedAsInteger($itemValue) && $itemValue != 0) {
// time(sec) is stored as elapsed seconds in DB, hence we interpret it as UTC time on 1970-01-01
// and pass on the ISO format to JS.
$itemValue = gmdate('c', (int)$itemValue);
}
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$expansionHtml = [];
$expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$expansionHtml[] = '<div class="form-wizards-wrap">';
$expansionHtml[] = '<div class="form-wizards-element">';
$expansionHtml[] = '<div class="input-group">';
$expansionHtml[] = '<input type="text" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
$expansionHtml[] = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
$expansionHtml[] = '<span class="input-group-btn">';
$expansionHtml[] = '<label class="btn btn-default" for="' . $attributes['id'] . '">';
$expansionHtml[] = $this->iconFactory->getIcon('actions-edit-pick-date', Icon::SIZE_SMALL)->render();
$expansionHtml[] = '</label>';
$expansionHtml[] = '</span>';
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
if (!empty($fieldControlHtml)) {
$expansionHtml[] = '<div class="form-wizards-items-aside">';
$expansionHtml[] = '<div class="btn-group">';
$expansionHtml[] = $fieldControlHtml;
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$expansionHtml[] = '<div class="form-wizards-items-bottom">';
$expansionHtml[] = $fieldWizardHtml;
$expansionHtml[] = '</div>';
}
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
$expansionHtml = implode(LF, $expansionHtml);
$fullElement = $expansionHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $expansionHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
$disabled = '';
$fallbackValue = 0;
if (strlen($placeholder) > 0) {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="' . $fallbackValue . '" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . $disabled . ' />';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $expansionHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = '<div class="formengine-field-item t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
return $resultArray;
}
/**
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,35 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
/**
* Generation of TCEform elements of the type "input type=hidden"
*/
class InputHiddenElement extends AbstractFormElement
{
/**
* This will render an input type="hidden" form field
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$resultArray['additionalHiddenFields'][] = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
return $resultArray;
}
}
@@ -0,0 +1,458 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
/**
* Link input element.
*
* Shows current link and the link popup.
*/
class InputLinkElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field controls render the link icon
*
* @var array
*/
protected $defaultFieldControl = [
'linkPopup' => [
'renderType' => 'linkPopup',
'options' => []
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
$itemValue = $parameterArray['itemFormElValue'];
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
$size = MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
$width = (int)$this->formMaxWidth($size);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']');
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly']) {
// Early return for read only fields
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
// @todo: The whole eval handling is a mess and needs refactoring
foreach ($evalList as $func) {
// @todo: This is ugly: The code should find out on it's own whether a eval definition is a
// @todo: keyword like "date", or a class reference. The global registration could be dropped then
// Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
if (class_exists($func)) {
$evalObj = GeneralUtility::makeInstance($func);
if (method_exists($evalObj, 'deevaluateFieldValue')) {
$_params = [
'value' => $itemValue
];
$itemValue = $evalObj->deevaluateFieldValue($_params);
}
if (method_exists($evalObj, 'returnFieldJS')) {
$resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($func) . ']'
. ' = function(value) {' . $evalObj->returnFieldJS() . '};';
}
}
}
}
$attributes = [
'value' => '',
'id' => StringUtility::getUniqueId('formengine-input-'),
'class' => implode(' ', [
'form-control',
't3js-clearable',
't3js-form-field-inputlink-input',
'hidden',
'hasDefaultValue',
]),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => json_encode([
'field' => $parameterArray['itemFormElName'],
'evalList' => implode(',', $evalList)
]),
'data-formengine-input-name' => $parameterArray['itemFormElName'],
];
$maxLength = $config['max'] ?? 0;
if ((int)$maxLength > 0) {
$attributes['maxlength'] = (int)$maxLength;
}
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
if (isset($config['autocomplete'])) {
$attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
}
$valuePickerHtml = [];
if (isset($config['valuePicker']['items']) && is_array($config['valuePicker']['items'])) {
$mode = $config['valuePicker']['mode'] ?? '';
$itemName = $parameterArray['itemFormElName'];
$fieldChangeFunc = $parameterArray['fieldChangeFunc'];
if ($mode === 'append') {
$assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
. '.value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
} elseif ($mode === 'prepend') {
$assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
. '.value+=\'\'+this.options[this.selectedIndex].value';
} else {
$assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
. '.value=this.options[this.selectedIndex].value';
}
$valuePickerHtml[] = '<select';
$valuePickerHtml[] = ' class="form-control tceforms-select tceforms-wizardselect"';
$valuePickerHtml[] = ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"';
$valuePickerHtml[] = '>';
$valuePickerHtml[] = '<option></option>';
foreach ($config['valuePicker']['items'] as $item) {
$valuePickerHtml[] = '<option value="' . htmlspecialchars($item[1]) . '">' . htmlspecialchars($languageService->sL($item[0])) . '</option>';
}
$valuePickerHtml[] = '</select>';
}
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$linkExplanation = $this->getLinkExplanation($itemValue ?: '');
$explanation = htmlspecialchars($linkExplanation['text']);
$toggleButtonTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.toggleLinkExplanation');
$expansionHtml = [];
$expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$expansionHtml[] = '<div class="form-wizards-wrap">';
$expansionHtml[] = '<div class="form-wizards-element">';
$expansionHtml[] = '<div class="input-group t3js-form-field-inputlink">';
$expansionHtml[] = '<span class="t3js-form-field-inputlink-icon input-group-addon">' . $linkExplanation['icon'] . '</span>';
$expansionHtml[] = '<input class="form-control form-field-inputlink-explanation t3js-form-field-inputlink-explanation" data-toggle="tooltip" data-title="' . $explanation . '" value="' . $explanation . '" readonly>';
$expansionHtml[] = '<input type="text" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
$expansionHtml[] = '<span class="input-group-btn">';
$expansionHtml[] = '<button class="btn btn-default t3js-form-field-inputlink-explanation-toggle" type="button" title="' . htmlspecialchars($toggleButtonTitle) . '">';
$expansionHtml[] = $this->iconFactory->getIcon('actions-version-workspaces-preview-link', Icon::SIZE_SMALL)->render();
$expansionHtml[] = '</button>';
$expansionHtml[] = '</span>';
$expansionHtml[] = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
if (!empty($valuePickerHtml) || !empty($fieldControlHtml)) {
$expansionHtml[] = '<div class="form-wizards-items-aside">';
$expansionHtml[] = '<div class="btn-group">';
$expansionHtml[] = implode(LF, $valuePickerHtml);
$expansionHtml[] = $fieldControlHtml;
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
}
$expansionHtml[] = '<div class="form-wizards-items-bottom">';
$expansionHtml[] = $linkExplanation['additionalAttributes'];
$expansionHtml[] = $fieldWizardHtml;
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
$expansionHtml = implode(LF, $expansionHtml);
$fullElement = $expansionHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $expansionHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
$disabled = '';
$fallbackValue = 0;
if (strlen($placeholder) > 0) {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="' . $fallbackValue . '" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . $disabled . ' />';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $expansionHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = '<div class="formengine-field-item t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
return $resultArray;
}
/**
* @param string $itemValue
* @return array
*/
protected function getLinkExplanation(string $itemValue): array
{
if (empty($itemValue)) {
return [];
}
$data = ['text' => '', 'icon' => ''];
$typolinkService = GeneralUtility::makeInstance(TypoLinkCodecService::class);
$linkParts = $typolinkService->decode($itemValue);
$linkService = GeneralUtility::makeInstance(LinkService::class);
try {
$linkData = $linkService->resolve($linkParts['url']);
} catch (FileDoesNotExistException $e) {
return $data;
} catch (FolderDoesNotExistException $e) {
return $data;
} catch (UnknownLinkHandlerException $e) {
return $data;
} catch (InvalidPathException $e) {
return $data;
}
// Resolving the TypoLink parts (class, title, params)
$additionalAttributes = [];
foreach ($linkParts as $key => $value) {
if ($key === 'url') {
continue;
}
if ($value) {
switch ($key) {
case 'class':
$label = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:class');
break;
case 'title':
$label = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:title');
break;
case 'additionalParams':
$label = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:params');
break;
default:
$label = $key;
}
$additionalAttributes[] = '<span><strong>' . htmlspecialchars($label) . ': </strong> ' . htmlspecialchars($value) . '</span>';
}
}
// Resolve the actual link
switch ($linkData['type']) {
case LinkService::TYPE_PAGE:
$pageRecord = BackendUtility::readPageAccess($linkData['pageuid'], '1=1');
// Is this a real page
if ($pageRecord['uid']) {
$data = [
'text' => $pageRecord['_thePathFull'] . '[' . $pageRecord['uid'] . ']',
'icon' => $this->iconFactory->getIconForRecord('pages', $pageRecord, Icon::SIZE_SMALL)->render()
];
}
break;
case LinkService::TYPE_EMAIL:
$data = [
'text' => $linkData['email'],
'icon' => $this->iconFactory->getIcon('content-elements-mailform', Icon::SIZE_SMALL)->render()
];
break;
case LinkService::TYPE_URL:
$data = [
'text' => $this->getDomainByUrl($linkData['url']),
'icon' => $this->iconFactory->getIcon('apps-pagetree-page-shortcut-external', Icon::SIZE_SMALL)->render()
];
break;
case LinkService::TYPE_FILE:
/** @var File $file */
$file = $linkData['file'];
if ($file) {
$data = [
'text' => $file->getPublicUrl(),
'icon' => $this->iconFactory->getIconForFileExtension($file->getExtension(), Icon::SIZE_SMALL)->render()
];
}
break;
case LinkService::TYPE_FOLDER:
/** @var Folder $folder */
$folder = $linkData['folder'];
if ($folder) {
$data = [
'text' => $folder->getPublicUrl(),
'icon' => $this->iconFactory->getIcon('apps-filetree-folder-default', Icon::SIZE_SMALL)->render()
];
}
break;
case LinkService::TYPE_RECORD:
$table = $this->data['pageTsConfig']['TCEMAIN.']['linkHandler.'][$linkData['identifier'] . '.']['configuration.']['table'];
$record = BackendUtility::getRecord($table, $linkData['uid']);
if ($record) {
$recordTitle = BackendUtility::getRecordTitle($table, $record);
$tableTitle = $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
$data = [
'text' => sprintf('%s [%s:%d]', $recordTitle, $tableTitle, $linkData['uid']),
'icon' => $this->iconFactory->getIconForRecord($table, $record, Icon::SIZE_SMALL)->render(),
];
} else {
$data = [
'text' => sprintf('%s', $linkData['uid']),
'icon' => $this->iconFactory->getIcon('tcarecords-' . $table . '-default', Icon::SIZE_SMALL, 'overlay-missing')->render(),
];
}
break;
default:
// Please note that this hook is preliminary and might change, as this element could become its own
// TCA type in the future
if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler'][$linkData['type']])) {
$linkBuilder = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler'][$linkData['type']]);
$data = $linkBuilder->getFormData($linkData, $linkParts, $this->data, $this);
} else {
$data = [
'text' => 'not implemented type ' . $linkData['type'],
'icon' => ''
];
}
}
$data['additionalAttributes'] = '<div class="help-block">' . implode(' - ', $additionalAttributes) . '</div>';
return $data;
}
/**
* @param string $uriString
*
* @return string
*/
protected function getDomainByUrl(string $uriString): string
{
$data = parse_url($uriString);
return $data['host'] ?? '';
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,254 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Controller\FormSlugAjaxController;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* General type=input element with some additional value.
*/
class InputSlugElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$table = $this->data['tableName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$languageId = 0;
if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($GLOBALS['TCA'][$table]['ctrl']['languageField'])) {
$languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
$languageId = (int)((is_array($row[$languageField]) ? $row[$languageField][0] : $row[$languageField]) ?? 0);
}
$baseUrl = $this->getPrefix($this->data['site'], $languageId);
$itemValue = $parameterArray['itemFormElValue'];
$config = $parameterArray['fieldConf']['config'];
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
$size = MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
$width = (int)$this->formMaxWidth($size);
// Convert UTF-8 characters back (that is important, see Slug class when sanitizing)
$itemValue = rawurldecode($itemValue);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$toggleButtonTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.toggleSlugExplanation');
$recreateButtonTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.recreateSlugExplanation');
$successMessage = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:slugCreation.success.' . ($table === 'pages' ? 'page' : 'record')), $baseUrl);
$errorMessage = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:slugCreation.error'), $baseUrl);
$thisSlugId = 't3js-form-field-slug-id' . StringUtility::getUniqueId();
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$mainFieldHtml[] = $fieldInformationHtml;
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px" id="' . htmlspecialchars($thisSlugId) . '">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-element">';
$mainFieldHtml[] = '<div class="input-group">';
$mainFieldHtml[] = ($baseUrl ? '<span class="input-group-addon">' . htmlspecialchars($baseUrl) . '</span>' : '');
// We deal with 3 fields here: a readonly field for current / default values, an input
// field to manipulate the value, and the final hidden field used to send the value
$mainFieldHtml[] = '<input';
$mainFieldHtml[] = ' class="form-control t3js-form-field-slug-readonly"';
$mainFieldHtml[] = ' data-toggle="tooltip"';
$mainFieldHtml[] = ' data-title="' . htmlspecialchars($itemValue) . '"';
$mainFieldHtml[] = ' value="' . htmlspecialchars($itemValue) . '"';
$mainFieldHtml[] = ' readonly';
$mainFieldHtml[] = ' />';
$mainFieldHtml[] = '<input type="text"';
$mainFieldHtml[] = ' id="' . htmlspecialchars(StringUtility::getUniqueId('formengine-input-')) . '"';
$mainFieldHtml[] = ' class="form-control t3js-form-field-slug-input hidden"';
$mainFieldHtml[] = ' placeholder="' . htmlspecialchars($row['slug'] ?? '/') . '"';
$mainFieldHtml[] = ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '"';
$mainFieldHtml[] = ' data-formengine-input-params="' . htmlspecialchars(json_encode(['field' => $parameterArray['itemFormElName'], 'evalList' => implode(',', $evalList)])) . '"';
$mainFieldHtml[] = ' data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$mainFieldHtml[] = ' />';
$mainFieldHtml[] = '<span class="input-group-btn">';
$mainFieldHtml[] = '<button class="btn btn-default t3js-form-field-slug-toggle" type="button" title="' . htmlspecialchars($toggleButtonTitle) . '">';
$mainFieldHtml[] = $this->iconFactory->getIcon('actions-version-workspaces-preview-link', Icon::SIZE_SMALL)->render();
$mainFieldHtml[] = '</button>';
$mainFieldHtml[] = '<button class="btn btn-default t3js-form-field-slug-recreate" type="button" title="' . htmlspecialchars($recreateButtonTitle) . '">';
$mainFieldHtml[] = $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render();
$mainFieldHtml[] = '</button>';
$mainFieldHtml[] = '</span>';
$mainFieldHtml[] = '<input type="hidden"';
$mainFieldHtml[] = ' class="t3js-form-field-slug-hidden"';
$mainFieldHtml[] = ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$mainFieldHtml[] = ' value="' . htmlspecialchars($itemValue) . '"';
$mainFieldHtml[] = ' />';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
if (!empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-items-aside">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '<div class="form-wizards-items-bottom">';
$mainFieldHtml[] = '<span class="t3js-form-proposal-accepted hidden label label-success">' . htmlspecialchars($successMessage) . '<span>/abc/</span></span>';
$mainFieldHtml[] = '<span class="t3js-form-proposal-different hidden label label-warning">' . htmlspecialchars($errorMessage) . '<span>/abc/</span></span>';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$resultArray['html'] = implode(LF, $mainFieldHtml);
[$commonElementPrefix] = GeneralUtility::revExplode('[', $parameterArray['itemFormElName'], 2);
$validInputNamesToListenTo = [];
$includeUidInValues = false;
foreach ($config['generatorOptions']['fields'] ?? [] as $fieldNameParts) {
if (is_string($fieldNameParts)) {
$fieldNameParts = GeneralUtility::trimExplode(',', $fieldNameParts);
}
foreach ($fieldNameParts as $listenerFieldName) {
if ($listenerFieldName === 'uid') {
$includeUidInValues = true;
continue;
}
$validInputNamesToListenTo[$listenerFieldName] = $commonElementPrefix . '[' . htmlspecialchars($listenerFieldName) . ']';
}
}
$parentPageId = $this->data['parentPageRow']['uid'] ?? 0;
$signature = GeneralUtility::hmac(
implode(
'',
[
$table,
$this->data['effectivePid'],
$row['uid'],
$languageId,
$this->data['fieldName'],
$this->data['command'],
$parentPageId
]
),
FormSlugAjaxController::class
);
$optionsForModule = [
'pageId' => $this->data['effectivePid'],
'recordId' => $row['uid'],
'tableName' => $table,
'fieldName' => $this->data['fieldName'],
'config' => $config,
'listenerFieldNames' => $validInputNamesToListenTo,
'language' => $languageId,
'originalValue' => $itemValue,
'signature' => $signature,
'command' => $this->data['command'],
'parentPageId' => $parentPageId,
'includeUidInValues' => $includeUidInValues,
];
$resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Element/SlugElement' => '
function(SlugElement) {
new SlugElement(' . GeneralUtility::quoteJSvalue('#' . $thisSlugId) . ', ' . json_encode($optionsForModule) . ');
}'
];
return $resultArray;
}
/**
* Render the prefix for the input field.
*
* @param SiteInterface $site
* @param int $requestLanguageId
* @return string
*/
protected function getPrefix(SiteInterface $site, int $requestLanguageId = 0): string
{
try {
$language = ($requestLanguageId < 0) ? $site->getDefaultLanguage() : $site->getLanguageById($requestLanguageId);
$base = $language->getBase();
$baseUrl = (string)$base;
$baseUrl = rtrim($baseUrl, '/');
if (!empty($baseUrl) && empty($base->getScheme()) && $base->getHost() !== '') {
$baseUrl = 'http:' . $baseUrl;
}
} catch (\InvalidArgumentException $e) {
// No site / language found
$baseUrl = '';
}
return $baseUrl;
}
/**
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,344 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* General type=input element.
*
* This one kicks in if no specific renderType like "inputDateTime"
* or "inputColorPicker" is set.
*/
class InputTextElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$itemValue = $parameterArray['itemFormElValue'];
$config = $parameterArray['fieldConf']['config'];
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
$size = MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
$width = (int)$this->formMaxWidth($size);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']');
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly']) {
// Early return for read only fields
if (in_array('password', $evalList, true)) {
$itemValue = $itemValue ? '*********' : '';
}
$disabledFieldAttributes = [
'class' => 'form-control',
'data-formengine-input-name' => $parameterArray['itemFormElName'],
'type' => 'text',
'value' => $itemValue,
];
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input ' . GeneralUtility::implodeAttributes($disabledFieldAttributes, true) . ' disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
// @todo: The whole eval handling is a mess and needs refactoring
foreach ($evalList as $func) {
// @todo: This is ugly: The code should find out on it's own whether a eval definition is a
// @todo: keyword like "date", or a class reference. The global registration could be dropped then
// Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
if (class_exists($func)) {
$evalObj = GeneralUtility::makeInstance($func);
if (method_exists($evalObj, 'deevaluateFieldValue')) {
$_params = [
'value' => $itemValue
];
$itemValue = $evalObj->deevaluateFieldValue($_params);
}
if (method_exists($evalObj, 'returnFieldJS')) {
$resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($func) . ']'
. ' = function(value) {' . $evalObj->returnFieldJS() . '};';
}
}
}
}
$fieldId = StringUtility::getUniqueId('formengine-input-');
$attributes = [
'value' => '',
'id' => $fieldId,
'class' => implode(' ', [
'form-control',
't3js-clearable',
'hasDefaultValue',
]),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => json_encode([
'field' => $parameterArray['itemFormElName'],
'evalList' => implode(',', $evalList),
'is_in' => trim($config['is_in'])
]),
'data-formengine-input-name' => $parameterArray['itemFormElName'],
];
$maxLength = $config['max'] ?? 0;
if ((int)$maxLength > 0) {
$attributes['maxlength'] = (int)$maxLength;
}
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
if (isset($config['autocomplete'])) {
$attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
}
$valuePickerHtml = [];
if (isset($config['valuePicker']['items']) && is_array($config['valuePicker']['items'])) {
$mode = $config['valuePicker']['mode'] ?? '';
$itemName = $parameterArray['itemFormElName'];
$fieldChangeFunc = $parameterArray['fieldChangeFunc'];
if ($mode === 'append') {
$assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
. '.value+=\'\'+this.options[this.selectedIndex].value';
} elseif ($mode === 'prepend') {
$assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
. '.value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
} else {
$assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
. '.value=this.options[this.selectedIndex].value';
}
$valuePickerHtml[] = '<select';
$valuePickerHtml[] = ' class="form-control tceforms-select tceforms-wizardselect"';
$valuePickerHtml[] = ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"';
$valuePickerHtml[] = '>';
$valuePickerHtml[] = '<option></option>';
foreach ($config['valuePicker']['items'] as $item) {
$valuePickerHtml[] = '<option value="' . htmlspecialchars($item[1]) . '">' . htmlspecialchars($languageService->sL($item[0])) . '</option>';
}
$valuePickerHtml[] = '</select>';
}
$valueSliderHtml = [];
if (isset($config['slider']) && is_array($config['slider'])) {
$id = 'slider-' . $fieldId;
$resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/FieldWizard/ValueSlider' =>
'function(ValueSlider) { new ValueSlider(' . GeneralUtility::quoteJSvalue($id) . '); }'
];
$min = $config['range']['lower'] ?? 0;
$max = $config['range']['upper'] ?? 10000;
$step = $config['slider']['step'] ?? 1;
$width = $config['slider']['width'] ?? 400;
$valueType = 'null';
if (in_array('int', $evalList, true)) {
$valueType = 'int';
$itemValue = (int)$itemValue;
} elseif (in_array('double2', $evalList, true)) {
$valueType = 'double';
$itemValue = (double)$itemValue;
}
$callbackParams = [ $table, $row['uid'], $fieldName, $parameterArray['itemFormElName'] ];
$valueSliderHtml[] = '<div';
$valueSliderHtml[] = ' id="' . $id . '"';
$valueSliderHtml[] = ' data-slider-id="' . $id . '"';
$valueSliderHtml[] = ' data-slider-min="' . (int)$min . '"';
$valueSliderHtml[] = ' data-slider-max="' . (int)$max . '"';
$valueSliderHtml[] = ' data-slider-step="' . htmlspecialchars($step) . '"';
$valueSliderHtml[] = ' data-slider-value="' . htmlspecialchars($itemValue) . '"';
$valueSliderHtml[] = ' data-slider-value-type="' . htmlspecialchars($valueType) . '"';
$valueSliderHtml[] = ' data-slider-item-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$valueSliderHtml[] = ' data-slider-callback-params="' . htmlspecialchars(json_encode($callbackParams)) . '"';
$valueSliderHtml[] = ' style="width: ' . $width . 'px;"';
$valueSliderHtml[] = '>';
$valueSliderHtml[] = '</div>';
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$inputType = 'text';
if (in_array('email', $evalList, true)) {
$inputType = 'email';
} elseif (!empty(array_intersect($evalList, ['int', 'num']))) {
$inputType = 'number';
if (isset($config['range']['lower'])) {
$attributes['min'] = (int)$config['range']['lower'];
}
if (isset($config['range']['upper'])) {
$attributes['max'] = (int)$config['range']['upper'];
}
}
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-element">';
$mainFieldHtml[] = '<input type="' . $inputType . '" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
$mainFieldHtml[] = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
$mainFieldHtml[] = '</div>';
if (!empty($valuePickerHtml) || !empty($valueSliderHtml) || !empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-items-aside">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = implode(LF, $valuePickerHtml);
$mainFieldHtml[] = implode(LF, $valueSliderHtml);
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-items-bottom">';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml = implode(LF, $mainFieldHtml);
$fullElement = $mainFieldHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $mainFieldHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
$disabled = '';
$fallbackValue = 0;
if (strlen($placeholder) > 0) {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="' . $fallbackValue . '" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . $disabled . ' />';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $mainFieldHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = '<div class="formengine-field-item t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
return $resultArray;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,80 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* None element is a "disabled" input element with formatted values if needed.
*/
class NoneElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* This will render a non-editable display of the content of the field.
*
* @return array The HTML code for the TCEform field
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$itemValue = $parameterArray['itemFormElValue'];
if (isset($config['format']) && $config['format']) {
$formatOptions = $config['format.'] ?? [];
$itemValue = $this->formatValue($config['format'], $itemValue, $formatOptions);
}
if (!$config['pass_content']) {
$itemValue = htmlspecialchars($itemValue);
}
$cols = $config['cols'] ?: ($config['size'] ?: $this->defaultInputWidth);
$size = MathUtility::forceIntegerInRange($cols ?: $this->defaultInputWidth, 5, $this->maxInputWidth);
$width = $this->formMaxWidth($size);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
/**
* PassThroughElement is the dummy element for type="passthrough".
* It does not render anything.
*/
class PassThroughElement extends AbstractFormElement
{
/**
* Return the empty initialized result array
*
* @return array
*/
public function render(): array
{
return $this->initializeResultArray();
}
}
@@ -0,0 +1,115 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
/**
* Render elements of type radio
*/
class RadioElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a series of radio buttons.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$resultArray = $this->initializeResultArray();
$disabled = '';
if ($this->data['parameterArray']['fieldConf']['config']['readOnly']) {
$disabled = ' disabled';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
foreach ($this->data['parameterArray']['fieldConf']['config']['items'] as $itemNumber => $itemLabelAndValue) {
$label = $itemLabelAndValue[0];
$value = $itemLabelAndValue[1];
$radioId = htmlspecialchars($this->data['parameterArray']['itemFormElID'] . '_' . $itemNumber);
$radioChecked = (string)$value === (string)$this->data['parameterArray']['itemFormElValue'] ? ' checked="checked"' : '';
$html[] = '<div class="radio' . $disabled . '">';
$html[] = '<label for="' . $radioId . '">';
$html[] = '<input type="radio"';
$html[] = ' name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '"';
$html[] = ' id="' . $radioId . '"';
$html[] = ' value="' . htmlspecialchars($value) . '"';
$html[] = $radioChecked;
$html[] = $disabled;
$html[] = ' onclick="' . htmlspecialchars(implode('', $this->data['parameterArray']['fieldChangeFunc'])) . '"';
$html[] = '/>';
$html[] = htmlspecialchars($this->appendValueToLabelInDebugMode($label, $value));
$html[] = '</label>';
$html[] = '</div>';
}
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
}
@@ -0,0 +1,264 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Creates a widget with check box elements.
*
* This is rendered for config type=select, renderType=selectCheckBox
*/
class SelectCheckBoxElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* Render check boxes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$resultArray = $this->initializeResultArray();
$html = [];
// Field configuration from TCA:
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$disabled = !empty($config['readOnly']);
$selItems = $config['items'];
if (!empty($selItems)) {
// Get values in an array (and make unique, which is fine because there can be no duplicates anyway)
// In case e.g. "l10n_display" is set to "defaultAsReadonly" only one value (as string) could be handed in
if (is_array($parameterArray['itemFormElValue'])) {
$itemArray = $parameterArray['itemFormElValue'];
} else {
$itemArray = [(string)$parameterArray['itemFormElValue']];
}
$itemArray = array_flip($itemArray);
// Traverse the Array of selector box items:
$groups = [];
$currentGroup = 0;
$c = 0;
$sOnChange = '';
if (!$disabled) {
$sOnChange = implode('', $parameterArray['fieldChangeFunc']);
// Used to accumulate the JS needed to restore the original selection.
foreach ($selItems as $p) {
// Non-selectable element:
if ($p[1] === '--div--') {
$selIcon = '';
if (isset($p[2]) && $p[2] !== 'empty-empty') {
$selIcon = FormEngineUtility::getIconHtml($p[2]);
}
$currentGroup++;
$groups[$currentGroup]['header'] = [
'icon' => $selIcon,
'title' => $p[0]
];
} else {
// Check if some help text is available
// Help text is expected to be an associative array
// with two key, "title" and "description"
// For the sake of backwards compatibility, we test if the help text
// is a string and use it as a description (this could happen if items
// are modified with an itemProcFunc)
$hasHelp = false;
$help = '';
$helpArray = [];
if (!empty($p[3])) {
$hasHelp = true;
if (is_array($p[3])) {
$helpArray = $p[3];
} else {
$helpArray['description'] = $p[3];
}
}
if ($hasHelp) {
$help = BackendUtility::wrapInHelp('', '', '', $helpArray);
}
// Selected or not by default:
$checked = 0;
if (isset($itemArray[$p[1]])) {
$checked = 1;
unset($itemArray[$p[1]]);
}
// Build item array
$groups[$currentGroup]['items'][] = [
'id' => StringUtility::getUniqueId('select_checkbox_row_'),
'name' => $parameterArray['itemFormElName'] . '[' . $c . ']',
'value' => $p[1],
'checked' => $checked,
'disabled' => false,
'class' => '',
'icon' => FormEngineUtility::getIconHtml(!empty($p[2]) ? $p[2] : 'empty-empty'),
'title' => $p[0],
'help' => $help
];
$c++;
}
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
// Add an empty hidden field which will send a blank value if all items are unselected.
$html[] = '<input type="hidden" class="select-checkbox" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="">';
// Building the checkboxes
foreach ($groups as $groupKey => $group) {
$groupId = htmlspecialchars($parameterArray['itemFormElID']) . '-group-' . $groupKey;
$html[] = '<div class="panel panel-default">';
if (is_array($group['header'])) {
$html[] = '<div class="panel-heading">';
$html[] = '<a data-toggle="collapse" href="#' . $groupId . '" aria-expanded="false" aria-controls="' . $groupId . '">';
$html[] = $group['header']['icon'];
$html[] = htmlspecialchars($group['header']['title']);
$html[] = '</a>';
$html[] = '</div>';
}
if (is_array($group['items']) && !empty($group['items'])) {
$tableRows = [];
$resetGroup = [];
// Render rows
foreach ($group['items'] as $item) {
$tableRows[] = '<tr class="' . $item['class'] . '">';
$tableRows[] = '<td class="col-checkbox">';
$tableRows[] = '<input type="checkbox" class="t3js-checkbox" '
. 'id="' . $item['id'] . '" '
. 'name="' . htmlspecialchars($item['name']) . '" '
. 'value="' . htmlspecialchars($item['value']) . '" '
. 'onclick="' . htmlspecialchars($sOnChange) . '" '
. ($item['checked'] ? 'checked=checked ' : '')
. ($item['disabled'] ? 'disabled=disabled ' : '') . '>';
$tableRows[] = '</td>';
$tableRows[] = '<td class="col-icon">';
$tableRows[] = '<label class="label-block" for="' . $item['id'] . '">' . $item['icon'] . '</label>';
$tableRows[] = '</td>';
$tableRows[] = '<td class="col-title">';
$tableRows[] = '<label class="label-block nowrap-disabled" for="' . $item['id'] . '">' . htmlspecialchars($this->appendValueToLabelInDebugMode($item['title'], $item['value']), ENT_COMPAT, 'UTF-8', false) . '</label>';
$tableRows[] = '</td>';
$tableRows[] = '<td class="text-right">' . $item['help'] . '</td>';
$tableRows[] = '</tr>';
$resetGroup[] = 'document.editform[' . GeneralUtility::quoteJSvalue($item['name']) . '].checked=' . $item['checked'] . ';';
}
// Build reset group button
$resetGroupBtn = '';
if (!empty($resetGroup)) {
$resetGroup[] = 'TYPO3.FormEngine.updateCheckboxState(this);';
$title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.revertSelection'));
$resetGroupBtn = '<a href="#" '
. 'class="btn btn-default btn-sm" '
. 'onclick="' . implode('', $resetGroup) . ' return false;" '
. 'title="' . $title . '">'
. $this->iconFactory->getIcon('actions-edit-undo', Icon::SIZE_SMALL)->render() . ' '
. $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.revertSelection') . '</a>';
}
if (is_array($group['header'])) {
$html[] = '<div id="' . $groupId . '" class="panel-collapse collapse" role="tabpanel">';
}
$checkboxId = uniqid($groupId);
$title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall'));
$html[] = '<div class="table-responsive">';
$html[] = '<table class="table table-transparent table-hover">';
$html[] = '<thead>';
$html[] = '<tr>';
$html[] = '<th class="col-checkbox">';
$html[] = '<input type="checkbox" id="' . $checkboxId . '" class="t3js-toggle-checkboxes" data-trigger="hover" data-placement="right" data-title="' . $title . '" data-toggle="tooltip" />';
$html[] = '</th>';
$html[] = '<th class="col-title" colspan="2"><label for="' . $checkboxId . '">' . $title . '</label></th>';
$html[] = '<th class="text-right">' . $resetGroupBtn . '</th>';
$html[] = '</tr>';
$html[] = '</thead>';
$html[] = '<tbody>' . implode(LF, $tableRows) . '</tbody>';
$html[] = '</table>';
$html[] = '</div>';
if (is_array($group['header'])) {
$html[] = '</div>';
}
}
$html[] = '</div>';
}
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
}
$resultArray['html'] = implode(LF, $html);
$resultArray['requireJsModules'][] = 'TYPO3/CMS/Backend/Tooltip';
return $resultArray;
}
}
@@ -0,0 +1,432 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render a widget with two boxes side by side.
*
* This is rendered for config type=select, renderType=selectMultipleSideBySide set
*/
class SelectMultipleSideBySideElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field controls for this element.
*
* @var array
*/
protected $defaultFieldControl = [
'editPopup' => [
'renderType' => 'editPopup',
'disabled' => true,
],
'addRecord' => [
'renderType' => 'addRecord',
'disabled' => true,
'after' => [ 'editPopup' ],
],
'listModule' => [
'renderType' => 'listModule',
'disabled' => true,
'after' => [ 'addRecord' ],
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* Render side by side element.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$elementName = $parameterArray['itemFormElName'];
if ($config['readOnly']) {
// Early return for the relatively simple read only case
return $this->renderReadOnly();
}
$possibleItems = $config['items'];
$selectedItems = $parameterArray['itemFormElValue'] ?: [];
$selectedItemsCount = count($selectedItems);
$maxItems = $config['maxitems'];
$autoSizeMax = MathUtility::forceIntegerInRange($config['autoSizeMax'], 0);
$size = 2;
if (isset($config['size'])) {
$size = (int)$config['size'];
}
if ($autoSizeMax >= 1) {
$size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
}
$itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
$listOfSelectedValues = [];
$selectedItemsHtml = [];
foreach ($selectedItems as $itemValue) {
foreach ($possibleItems as $possibleItem) {
if ($possibleItem[1] == $itemValue) {
$title = $possibleItem[0];
$listOfSelectedValues[] = $itemValue;
$selectedItemsHtml[] = '<option value="' . htmlspecialchars($itemValue) . '" title="' . htmlspecialchars($title) . '">' . htmlspecialchars($this->appendValueToLabelInDebugMode($title, $itemValue)) . '</option>';
break;
}
}
}
$selectableItemsHtml = [];
foreach ($possibleItems as $possibleItem) {
$disabledAttr = '';
$classAttr = '';
if (!$itemCanBeSelectedMoreThanOnce && in_array((string)$possibleItem[1], $selectedItems, true)) {
$disabledAttr = ' disabled="disabled"';
$classAttr = ' class="hidden"';
}
$selectableItemsHtml[] =
'<option value="'
. htmlspecialchars($possibleItem[1])
. '" title="' . htmlspecialchars($possibleItem[0]) . '"'
. $classAttr . $disabledAttr
. '>'
. htmlspecialchars($this->appendValueToLabelInDebugMode($possibleItem[0], $possibleItem[1])) .
'</option>';
}
// Html stuff for filter and select filter on top of right side of multi select boxes
$filterTextfield = [];
if ($config['enableMultiSelectFilterTextfield']) {
$filterTextfield[] = '<span class="input-group input-group-sm">';
$filterTextfield[] = '<span class="input-group-addon">';
$filterTextfield[] = '<span class="fa fa-filter"></span>';
$filterTextfield[] = '</span>';
$filterTextfield[] = '<input class="t3js-formengine-multiselect-filter-textfield form-control" value="">';
$filterTextfield[] = '</span>';
}
$filterDropDownOptions = [];
if (isset($config['multiSelectFilterItems']) && is_array($config['multiSelectFilterItems']) && count($config['multiSelectFilterItems']) > 1) {
foreach ($config['multiSelectFilterItems'] as $optionElement) {
$value = $languageService->sL($optionElement[0]);
$label = $value;
if (isset($optionElement[1]) && trim($optionElement[1]) !== '') {
$label = $languageService->sL($optionElement[1]);
}
$filterDropDownOptions[] = '<option value="' . htmlspecialchars($value) . '">' . htmlspecialchars($label) . '</option>';
}
}
$filterHtml = [];
if (!empty($filterTextfield) || !empty($filterDropDownOptions)) {
$filterHtml[] = '<div class="form-multigroup-item-wizard">';
if (!empty($filterTextfield) && !empty($filterDropDownOptions)) {
$filterHtml[] = '<div class="t3js-formengine-multiselect-filter-container form-multigroup-wrap">';
$filterHtml[] = '<div class="form-multigroup-item form-multigroup-element">';
$filterHtml[] = '<select class="form-control input-sm t3js-formengine-multiselect-filter-dropdown">';
$filterHtml[] = implode(LF, $filterDropDownOptions);
$filterHtml[] = '</select>';
$filterHtml[] = '</div>';
$filterHtml[] = '<div class="form-multigroup-item form-multigroup-element">';
$filterHtml[] = implode(LF, $filterTextfield);
$filterHtml[] = '</div>';
$filterHtml[] = '</div>';
} elseif (!empty($filterTextfield)) {
$filterHtml[] = implode(LF, $filterTextfield);
} else {
$filterHtml[] = '<select class="form-control input-sm t3js-formengine-multiselect-filter-dropdown">';
$filterHtml[] = implode(LF, $filterDropDownOptions);
$filterHtml[] = '</select>';
}
$filterHtml[] = '</div>';
}
$classes = [];
$classes[] = 'form-control';
$classes[] = 'tceforms-multiselect';
if ($maxItems === 1) {
$classes[] = 'form-select-no-siblings';
}
$multipleAttribute = '';
if ($maxItems !== 1 && $size !== 1) {
$multipleAttribute = ' multiple="multiple"';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<input type="hidden" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . (int)$itemCanBeSelectedMoreThanOnce . '" />';
$html[] = '<div class="form-multigroup-wrap t3js-formengine-field-group">';
$html[] = '<div class="form-multigroup-item form-multigroup-element">';
$html[] = '<label>';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selected'));
$html[] = '</label>';
$html[] = '<div class="form-wizards-wrap form-wizards-aside">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<select';
$html[] = ' id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
$html[] = ' size="' . $size . '"';
$html[] = ' class="' . implode(' ', $classes) . '"';
$html[] = $multipleAttribute;
$html[] = ' data-formengine-input-name="' . htmlspecialchars($elementName) . '"';
$html[] = '>';
$html[] = implode(LF, $selectedItemsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '<div class="form-wizards-items-aside">';
$html[] = '<div class="btn-group-vertical">';
if ($maxItems > 1 && $size >= 5) {
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-moveoption-top"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-top', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
}
if ($maxItems > 1) {
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-moveoption-up"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-moveoption-down"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
}
if ($maxItems > 1 && $size >= 5) {
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-moveoption-bottom"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-bottom', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
}
$html[] = '<a href="#"';
$html[] = ' class="btn btn-default t3js-btn-removeoption"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-selection-delete', Icon::SIZE_SMALL)->render();
$html[] = '</a>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<div class="form-multigroup-item form-multigroup-element">';
$html[] = '<label>';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.items'));
$html[] = '</label>';
$html[] = '<div class="form-wizards-wrap form-wizards-aside">';
$html[] = '<div class="form-wizards-element">';
$html[] = implode(LF, $filterHtml);
$html[] = '<select';
$html[] = ' data-relatedfieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' data-exclusivevalues="' . htmlspecialchars($config['exclusiveKeys']) . '"';
$html[] = ' id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
$html[] = ' data-formengine-input-name="' . htmlspecialchars($elementName) . '"';
$html[] = ' class="form-control t3js-formengine-select-itemstoselect"';
$html[] = ' size="' . $size . '"';
$html[] = ' onchange="' . htmlspecialchars(implode('', $parameterArray['fieldChangeFunc'])) . '"';
$html[] = ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '"';
$html[] = '>';
$html[] = implode(LF, $selectableItemsHtml);
$html[] = '</select>';
$html[] = '</div>';
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-items-aside">';
$html[] = '<div class="btn-group-vertical">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<input type="hidden" name="' . htmlspecialchars($elementName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
$html[] = '</div>';
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* Create HTML of a read only multi select. Right side is not
* rendered, but just the left side with the selected items.
*
* @return array
*/
protected function renderReadOnly()
{
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$fieldName = $parameterArray['itemFormElName'];
$possibleItems = $config['items'];
$selectedItems = $parameterArray['itemFormElValue'] ?: [];
if (!is_array($selectedItems)) {
$selectedItems = GeneralUtility::trimExplode(',', $selectedItems, true);
}
$selectedItemsCount = count($selectedItems);
$autoSizeMax = MathUtility::forceIntegerInRange($config['autoSizeMax'], 0);
$size = 2;
if (isset($config['size'])) {
$size = (int)$config['size'];
}
if ($autoSizeMax >= 1) {
$size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
}
$multiple = '';
if ($size !== 1) {
$multiple = ' multiple="multiple"';
}
$listOfSelectedValues = [];
$optionsHtml = [];
foreach ($selectedItems as $itemValue) {
foreach ($possibleItems as $possibleItem) {
if ($possibleItem[1] == $itemValue) {
$title = $possibleItem[0];
$listOfSelectedValues[] = $itemValue;
$optionsHtml[] = '<option value="' . htmlspecialchars($itemValue) . '" title="' . htmlspecialchars($title) . '">' . htmlspecialchars($title) . '</option>';
break;
}
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<label>';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selected'));
$html[] = '</label>';
$html[] = '<div class="form-wizards-wrap form-wizards-aside">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<select';
$html[] = ' id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
$html[] = ' size="' . $size . '"';
$html[] = ' class="form-control tceforms-multiselect"';
$html[] = $multiple;
$html[] = ' data-formengine-input-name="' . htmlspecialchars($fieldName) . '"';
$html[] = ' disabled="disabled">';
$html[] = '/>';
$html[] = implode(LF, $optionsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<input type="hidden" name="' . htmlspecialchars($fieldName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,224 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Create a widget with a select box where multiple items can be selected
*
* This is rendered for config type=select, renderType=selectSingleBox
*/
class SelectSingleBoxElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field controls for this element.
*
* @var array
*/
protected $defaultFieldControl = [
'resetSelection' => [
'renderType' => 'resetSelection',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a selector box element, or possibly a special construction with two selector boxes.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
// Field configuration from TCA:
$config = $parameterArray['fieldConf']['config'];
$selectItems = $parameterArray['fieldConf']['config']['items'];
$disabled = !empty($config['readOnly']);
// Get values in an array (and make unique, which is fine because there can be no duplicates anyway):
$itemArray = array_flip($parameterArray['itemFormElValue']);
$width = $this->formMaxWidth($this->defaultInputWidth);
$optionElements = [];
foreach ($selectItems as $i => $item) {
$value = $item[1];
$attributes = [];
// Selected or not by default
if (isset($itemArray[$value])) {
$attributes['selected'] = 'selected';
unset($itemArray[$value]);
}
// Non-selectable element
if ((string)$value === '--div--') {
$attributes['disabled'] = 'disabled';
$attributes['class'] = 'formcontrol-select-divider';
}
$optionElements[] = $this->renderOptionElement($value, $item[0], $attributes);
}
$selectElement = $this->renderSelectElement($optionElements, $parameterArray, $config);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<div class="form-wizards-wrap form-wizards-aside">';
$html[] = '<div class="form-wizards-element">';
if (!$disabled) {
// Add an empty hidden field which will send a blank value if all items are unselected.
$html[] = '<input type="hidden" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="">';
}
$html[] = $selectElement;
$html[] = '</div>';
if (!$disabled) {
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-items-aside">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '<p>';
$html[] = '<em>' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.holdDownCTRL')) . '</em>';
$html[] = '</p>';
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* Renders a <select> element
*
* @param array $optionElements List of rendered <option> elements
* @param array $parameterArray
* @param array $config Field configuration
* @return string
*/
protected function renderSelectElement(array $optionElements, array $parameterArray, array $config)
{
$selectItems = $parameterArray['fieldConf']['config']['items'];
$size = (int)$config['size'];
$cssPrefix = $size === 1 ? 'tceforms-select' : 'tceforms-multiselect';
if ($config['autoSizeMax']) {
$size = MathUtility::forceIntegerInRange(
count($selectItems) + 1,
MathUtility::forceIntegerInRange($size, 1),
$config['autoSizeMax']
);
}
$attributes = [
'name' => $parameterArray['itemFormElName'] . '[]',
'multiple' => 'multiple',
'onchange' => implode('', $parameterArray['fieldChangeFunc']),
'id' => StringUtility::getUniqueId($cssPrefix),
'class' => 'form-control ' . $cssPrefix,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
];
if ($size) {
$attributes['size'] = $size;
}
if ($config['readOnly']) {
$attributes['disabled'] = 'disabled';
}
$html = [];
$html[] = '<select ' . GeneralUtility::implodeAttributes($attributes, true) . '>';
$html[] = implode(LF, $optionElements);
$html[] = '</select>';
return implode(LF, $html);
}
/**
* Renders a single <option> element
*
* @param string $value The option value
* @param string $label The option label
* @param array $attributes Map of attribute names and values
* @return string
*/
protected function renderOptionElement($value, $label, array $attributes = [])
{
$attributes['value'] = $value;
$html = [
'<option ' . GeneralUtility::implodeAttributes($attributes, true) . '>',
htmlspecialchars($this->appendValueToLabelInDebugMode($label, $value), ENT_COMPAT, 'UTF-8', false),
'</option>'
];
return implode('', $html);
}
}
@@ -0,0 +1,268 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Creates a widget where only one item can be selected.
* This is either a select drop-down if no size config is given or set to 1, or a select box.
*
* This is rendered for type=select, renderType=selectSingle
*/
class SelectSingleElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'selectIcons' => [
'renderType' => 'selectIcons',
'disabled' => true,
],
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
'after' => [
'selectIcons',
],
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [ 'localizationStateSelector' ],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [ 'otherLanguageContent' ],
],
];
/**
* Render single element
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$resultArray = $this->initializeResultArray();
$table = $this->data['tableName'];
$field = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$selectItems = $parameterArray['fieldConf']['config']['items'];
// Check against inline uniqueness
/** @var InlineStackProcessor $inlineStackProcessor */
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
$uniqueIds = null;
if ($this->data['isInlineChild'] && $this->data['inlineParentUid']) {
// @todo: At least parts of this if is dead and/or broken: $uniqueIds is filled but never used.
// See InlineControlContainer where 'inlineData' 'unique' 'used' is set. What exactly is
// this if supposed to do and when should it kick in and what for?
$inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
$inlineFormName = $inlineStackProcessor->getCurrentStructureFormPrefix();
if ($this->data['inlineParentConfig']['foreign_table'] === $table
&& $this->data['inlineParentConfig']['foreign_unique'] === $field
) {
$uniqueIds = $this->data['inlineData']['unique'][$inlineObjectName . '-' . $table]['used'];
$parameterArray['fieldChangeFunc']['inlineUnique'] = 'inline.updateUnique(this,'
. GeneralUtility::quoteJSvalue($inlineObjectName . '-' . $table) . ','
. GeneralUtility::quoteJSvalue($inlineFormName) . ','
. GeneralUtility::quoteJSvalue($row['uid']) . ');';
}
// hide uid of parent record for symmetric relations
if ($this->data['inlineParentConfig']['foreign_table'] === $table
&& (
$this->data['inlineParentConfig']['foreign_field'] === $field
|| $this->data['inlineParentConfig']['symmetric_field'] === $field
)
) {
$uniqueIds[] = $this->data['inlineParentUid'];
}
}
// Initialization:
$selectId = StringUtility::getUniqueId('tceforms-select-');
$selectedIcon = '';
$size = (int)$config['size'];
// Style set on <select/>
$options = '';
$disabled = false;
if (!empty($config['readOnly'])) {
$disabled = true;
}
// Prepare groups
$selectItemCounter = 0;
$selectItemGroupCount = 0;
$selectItemGroups = [];
$selectedValue = '';
$hasIcons = false;
// In case e.g. "l10n_display" is set to "defaultAsReadonly" only one value (as string) could be handed in
if (!empty($parameterArray['itemFormElValue'])) {
if (is_array($parameterArray['itemFormElValue'])) {
$selectedValue = (string)$parameterArray['itemFormElValue'][0];
} else {
$selectedValue = (string)$parameterArray['itemFormElValue'];
}
}
foreach ($selectItems as $item) {
if ($item[1] === '--div--') {
// IS OPTGROUP
if ($selectItemCounter !== 0) {
$selectItemGroupCount++;
}
$selectItemGroups[$selectItemGroupCount]['header'] = [
'title' => $item[0],
];
} else {
// IS ITEM
$icon = !empty($item[2]) ? FormEngineUtility::getIconHtml($item[2], $item[0], $item[0]) : '';
$selected = $selectedValue === (string)$item[1];
if ($selected) {
$selectedIcon = $icon;
}
$selectItemGroups[$selectItemGroupCount]['items'][] = [
'title' => $this->appendValueToLabelInDebugMode($item[0], $item[1]),
'value' => $item[1],
'icon' => $icon,
'selected' => $selected,
];
$selectItemCounter++;
}
}
// Fallback icon
// @todo: assign a special icon for non matching values?
if (!$selectedIcon && $selectItemGroups[0]['items'][0]['icon']) {
$selectedIcon = $selectItemGroups[0]['items'][0]['icon'];
}
// Process groups
foreach ($selectItemGroups as $selectItemGroup) {
// suppress groups without items
if (empty($selectItemGroup['items'])) {
continue;
}
$optionGroup = is_array($selectItemGroup['header']);
$options .= ($optionGroup ? '<optgroup label="' . htmlspecialchars($selectItemGroup['header']['title'], ENT_COMPAT, 'UTF-8', false) . '">' : '');
if (is_array($selectItemGroup['items'])) {
foreach ($selectItemGroup['items'] as $item) {
$options .= '<option value="' . htmlspecialchars($item['value']) . '" data-icon="' .
htmlspecialchars($item['icon']) . '"'
. ($item['selected'] ? ' selected="selected"' : '') . '>' . htmlspecialchars($item['title'], ENT_COMPAT, 'UTF-8', false) . '</option>';
}
$hasIcons = !empty($item['icon']);
}
$options .= ($optionGroup ? '</optgroup>' : '');
}
$selectAttributes = [
'id' => $selectId,
'name' => $parameterArray['itemFormElName'],
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'class' => 'form-control form-control-adapt',
];
if ($size) {
$selectAttributes['size'] = $size;
}
if ($disabled) {
$selectAttributes['disabled'] = 'disabled';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
if ($hasIcons) {
$html[] = '<div class="input-group">';
$html[] = '<span class="input-group-addon input-group-icon">';
$html[] = $selectedIcon;
$html[] = '</span>';
}
$html[] = '<select ' . GeneralUtility::implodeAttributes($selectAttributes, true) . '>';
$html[] = $options;
$html[] = '</select>';
if ($hasIcons) {
$html[] = '</div>';
}
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Element/SelectSingleElement' => implode(LF, [
'function(SelectSingleElement) {',
'require([\'jquery\'], function($) {',
'$(function() {',
'SelectSingleElement.initialize(',
GeneralUtility::quoteJSvalue('#' . $selectId) . ',',
'{',
'onChange: function() {',
implode('', $parameterArray['fieldChangeFunc']),
'}',
'}',
');',
'});',
'});',
'}',
])];
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
}
@@ -0,0 +1,204 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
/**
* Render data as a tree.
*
* Typically rendered for config type=select, renderType=selectTree
*/
class SelectTreeElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* @var array Default wizards
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
];
/**
* Default number of tree nodes to show (determines tree height)
* when no ['config']['size'] is set
*
* @var int
*/
protected $itemsToShow = 15;
/**
* Number of items to show at last
* e.g. when you have only 2 items in a tree
*
* @var int
*/
protected $minItemsToShow = 5;
/**
* Pixel height of a single tree node
*
* @var int
*/
protected $itemHeight = 20;
/**
* Render tree widget
*
* @return array As defined in initializeResultArray() of AbstractNode
* @see AbstractNode::initializeResultArray()
*/
public function render()
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$formElementId = md5($parameterArray['itemFormElName']);
// Field configuration from TCA:
$config = $parameterArray['fieldConf']['config'];
$readOnly = !empty($config['readOnly']) ? 'true' : 'false';
$exclusiveKeys = !empty($config['exclusiveKeys']) ? $config['exclusiveKeys'] : '';
$exclusiveKeys = $exclusiveKeys . ',';
$appearance = !empty($config['treeConfig']['appearance']) ? $config['treeConfig']['appearance'] : [];
$expanded = !empty($appearance['expandAll']);
$showHeader = !empty($appearance['showHeader']);
if (isset($config['size']) && (int)$config['size'] > 0) {
$height = max($this->minItemsToShow, (int)$config['size']);
} else {
$height = $this->itemsToShow;
}
$heightInPx = $height * $this->itemHeight;
$treeWrapperId = 'tree_' . $formElementId;
$fieldName = $this->data['fieldName'];
$dataStructureIdentifier = '';
$flexFormSheetName = '';
$flexFormFieldName = '';
$flexFormContainerName = '';
$flexFormContainerIdentifier = '';
$flexFormContainerFieldName = '';
$flexFormSectionContainerIsNew = false;
if ($this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
$dataStructureIdentifier = $this->data['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'];
if (isset($this->data['flexFormSheetName'])) {
$flexFormSheetName = $this->data['flexFormSheetName'];
}
if (isset($this->data['flexFormFieldName'])) {
$flexFormFieldName = $this->data['flexFormFieldName'];
}
if (isset($this->data['flexFormContainerName'])) {
$flexFormContainerName = $this->data['flexFormContainerName'];
}
if (isset($this->data['flexFormContainerFieldName'])) {
$flexFormContainerFieldName = $this->data['flexFormContainerFieldName'];
}
if (isset($this->data['flexFormContainerIdentifier'])) {
$flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier'];
}
// Add a flag this is a tree in a new flex section container element. This is needed to initialize
// the databaseRow with this container again so the tree data provider is able to calculate tree items.
if (!empty($this->data['flexSectionContainerPreparation'])) {
$flexFormSectionContainerIsNew = true;
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<div class="typo3-tceforms-tree">';
$html[] = '<input class="treeRecord" type="hidden"';
$html[] = ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '"';
$html[] = ' data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$html[] = ' data-tablename="' . htmlspecialchars($this->data['tableName']) . '"';
$html[] = ' data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"';
$html[] = ' data-uid="' . (int)$this->data['vanillaUid'] . '"';
$html[] = ' data-recordtypevalue="' . htmlspecialchars($this->data['recordTypeValue']) . '"';
$html[] = ' data-datastructureidentifier="' . htmlspecialchars($dataStructureIdentifier) . '"';
$html[] = ' data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
$html[] = ' data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
$html[] = ' data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
$html[] = ' data-flexformcontaineridentifier="' . htmlspecialchars($flexFormContainerIdentifier) . '"';
$html[] = ' data-flexformcontainerfieldname="' . htmlspecialchars($flexFormContainerFieldName) . '"';
$html[] = ' data-flexformsectioncontainerisnew="' . htmlspecialchars($flexFormSectionContainerIsNew) . '"';
$html[] = ' data-command="' . htmlspecialchars($this->data['command']) . '"';
$html[] = ' data-read-only="' . $readOnly . '"';
$html[] = ' data-tree-exclusive-keys="' . htmlspecialchars($exclusiveKeys) . '"';
$html[] = ' data-tree-expand-up-to-level="' . ($expanded ? '999' : '1') . '"';
$html[] = ' data-tree-show-toolbar="' . $showHeader . '"';
$html[] = ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$html[] = ' id="treeinput' . $formElementId . '"';
$html[] = ' value="' . htmlspecialchars(implode(',', $parameterArray['itemFormElValue'])) . '"';
$html[] = '/>';
$html[] = '</div>';
$html[] = '<div id="' . $treeWrapperId . '" class="svg-tree-wrapper" style="height: ' . $heightInPx . 'px;"></div>';
$html[] = '<script type="text/javascript">var ' . $treeWrapperId . ' = ' . $this->getTreeOnChangeJs() . '</script>';
$html[] = '</div>';
if ($readOnly === 'false' && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
// add necessary labels for tree header
if ($showHeader) {
$resultArray['additionalInlineLanguageLabelFiles'][] = 'EXT:core/Resources/Private/Language/locallang_csh_corebe.xlf';
}
$resultArray['requireJsModules']['selectTreeElement'] = [
'TYPO3/CMS/Backend/FormEngine/Element/SelectTreeElement' => 'function (SelectTreeElement) { SelectTreeElement.initialize(); }'
];
return $resultArray;
}
/**
* Generates JS code triggered on change of the tree
*
* @return string
*/
protected function getTreeOnChangeJs()
{
$parameterArray = $this->data['parameterArray'];
$onChange = !empty($parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']) ? $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] : '';
$onChange .= !empty($parameterArray['fieldChangeFunc']['alert']) ? $parameterArray['fieldChangeFunc']['alert'] : '';
return 'function () {' . $onChange . '}';
}
}
@@ -0,0 +1,323 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of TCEform elements of the type "text"
*/
class TextElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* The number of chars expected per row when the height of a text area field is
* automatically calculated based on the number of characters found in the field content.
*
* @var int
*/
protected $charactersPerRow = 40;
/**
* This will render a <textarea>
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUserAuthentication();
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$itemValue = $parameterArray['itemFormElValue'];
$config = $parameterArray['fieldConf']['config'];
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
$cols = MathUtility::forceIntegerInRange($config['cols'] ?: $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
$width = $this->formMaxWidth($cols);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']');
// Setting number of rows
$rows = MathUtility::forceIntegerInRange($config['rows'] ?: 5, 1, 20);
$originalRows = $rows;
$itemFormElementValueLength = strlen($itemValue);
if ($itemFormElementValueLength > $this->charactersPerRow * 2) {
$rows = MathUtility::forceIntegerInRange(
round($itemFormElementValueLength / $this->charactersPerRow),
count(explode(LF, $itemValue)),
20
);
if ($rows < $originalRows) {
$rows = $originalRows;
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly']) {
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<textarea class="form-control" rows="' . $rows . '" disabled>';
$html[] = htmlspecialchars($itemValue);
$html[] = '</textarea>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
// @todo: The whole eval handling is a mess and needs refactoring
foreach ($evalList as $func) {
// @todo: This is ugly: The code should find out on it's own whether a eval definition is a
// @todo: keyword like "date", or a class reference. The global registration could be dropped then
// Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
// There is a similar hook for "evaluateFieldValue" in DataHandler and InputTextElement
if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
if (class_exists($func)) {
$evalObj = GeneralUtility::makeInstance($func);
if (method_exists($evalObj, 'deevaluateFieldValue')) {
$_params = [
'value' => $itemValue
];
$itemValue = $evalObj->deevaluateFieldValue($_params);
}
}
}
}
$attributes = [
'id' => StringUtility::getUniqueId('formengine-textarea-'),
'name' => htmlspecialchars($parameterArray['itemFormElName']),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-name' => htmlspecialchars($parameterArray['itemFormElName']),
'rows' => $rows,
'wrap' => $config['wrap'] ?: 'virtual',
'onChange' => implode('', $parameterArray['fieldChangeFunc']),
];
$classes = [
'form-control',
't3js-formengine-textarea',
'formengine-textarea',
];
if ($config['fixedFont']) {
$classes[] = 'text-monospace';
}
if ($config['enableTabulator']) {
$classes[] = 't3js-enable-tab';
}
$attributes['class'] = implode(' ', $classes);
$maximumHeight = (int)$backendUser->uc['resizeTextareas_MaxHeight'];
if ($maximumHeight > 0) {
// add the max-height from the users' preference to it
$attributes['style'] = 'max-height: ' . $maximumHeight . 'px';
}
if (isset($config['max']) && (int)$config['max'] > 0) {
$attributes['maxlength'] = (int)$config['max'];
}
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = htmlspecialchars(trim($config['placeholder']));
}
$valuePickerHtml = [];
if (isset($config['valuePicker']['items']) && is_array($config['valuePicker']['items'])) {
$mode = $config['valuePicker']['mode'] ?? '';
$itemName = $parameterArray['itemFormElName'];
$fieldChangeFunc = $parameterArray['fieldChangeFunc'];
if ($mode === 'append') {
$assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
. '.value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
} elseif ($mode === 'prepend') {
$assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
. '.value+=\'\'+this.options[this.selectedIndex].value';
} else {
$assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
. '.value=this.options[this.selectedIndex].value';
}
$valuePickerHtml[] = '<select';
$valuePickerHtml[] = ' class="form-control tceforms-select tceforms-wizardselect"';
$valuePickerHtml[] = ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"';
$valuePickerHtml[] = '>';
$valuePickerHtml[] = '<option></option>';
foreach ($config['valuePicker']['items'] as $item) {
$valuePickerHtml[] = '<option value="' . htmlspecialchars($item[1]) . '">' . htmlspecialchars($languageService->sL($item[0])) . '</option>';
}
$valuePickerHtml[] = '</select>';
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-element">';
$mainFieldHtml[] = '<textarea ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . htmlspecialchars($itemValue) . '</textarea>';
$mainFieldHtml[] = '</div>';
if (!empty($valuePickerHtml) || !empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-items-aside">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = implode(LF, $valuePickerHtml);
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-items-bottom">';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml = implode(LF, $mainFieldHtml);
$fullElement = $mainFieldHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $mainFieldHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
$disabled = '';
$fallbackValue = 0;
if (strlen($placeholder) > 0) {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="' . $fallbackValue . '" />';
$fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . $disabled . ' />';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<textarea';
$fullElement[] = ' class="form-control formengine-textarea' . (isset($config['fixedFont']) ? ' text-monospace' : '') . '"';
$fullElement[] = ' disabled="disabled"';
$fullElement[] = ' rows="' . htmlspecialchars($attributes['rows']) . '"';
$fullElement[] = ' wrap="' . htmlspecialchars($attributes['wrap']) . '"';
$fullElement[] = isset($attributes['style']) ? ' style="' . htmlspecialchars($attributes['style']) . '"' : '';
$fullElement[] = isset($attributes['maxlength']) ? ' maxlength="' . htmlspecialchars($attributes['maxlength']) . '"' : '';
$fullElement[] = '>';
$fullElement[] = htmlspecialchars($shortenedPlaceholder);
$fullElement[] = '</textarea>';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $mainFieldHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = '<div class="formengine-field-item t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
return $resultArray;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,230 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of TCEform elements of the type "text"
*/
class TextTableElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This renderType adds the "table wizard icon" by default (on not new records)
*
* @var array
*/
protected $defaultFieldControl = [
'tableWizard' => [
'renderType' => 'tableWizard',
],
];
/**
* The number of chars expected per row when the height of a text area field is
* automatically calculated based on the number of characters found in the field content.
*
* @var int
*/
protected $charactersPerRow = 40;
/**
* This will render a <textarea> with table wizard
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$backendUser = $this->getBackendUserAuthentication();
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$itemValue = $parameterArray['itemFormElValue'];
$config = $parameterArray['fieldConf']['config'];
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
$cols = MathUtility::forceIntegerInRange($config['cols'] ?: $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
$width = $this->formMaxWidth($cols);
// Setting number of rows
$rows = MathUtility::forceIntegerInRange($config['rows'] ?: 5, 1, 20);
$originalRows = $rows;
$itemFormElementValueLength = strlen($itemValue);
if ($itemFormElementValueLength > $this->charactersPerRow * 2) {
$rows = MathUtility::forceIntegerInRange(
round($itemFormElementValueLength / $this->charactersPerRow),
count(explode(LF, $itemValue)),
20
);
if ($rows < $originalRows) {
$rows = $originalRows;
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly']) {
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<textarea class="form-control" rows="' . $rows . '" disabled>';
$html[] = htmlspecialchars($itemValue);
$html[] = '</textarea>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
// @todo: The whole eval handling is a mess and needs refactoring
foreach ($evalList as $func) {
// @todo: This is ugly: The code should find out on it's own whether a eval definition is a
// @todo: keyword like "date", or a class reference. The global registration could be dropped then
// Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
// There is a similar hook for "evaluateFieldValue" in DataHandler and InputTextElement
if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
if (class_exists($func)) {
$evalObj = GeneralUtility::makeInstance($func);
if (method_exists($evalObj, 'deevaluateFieldValue')) {
$_params = [
'value' => $itemValue
];
$itemValue = $evalObj->deevaluateFieldValue($_params);
}
}
}
}
$attributes = [
'id' => StringUtility::getUniqueId('formengine-textarea-'),
'name' => htmlspecialchars($parameterArray['itemFormElName']),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-name' => htmlspecialchars($parameterArray['itemFormElName']),
'rows' => $rows,
'wrap' => $config['wrap'] ?: 'virtual',
'onChange' => implode('', $parameterArray['fieldChangeFunc']),
];
$classes = [
'form-control',
't3js-formengine-textarea',
'formengine-textarea',
];
if ($config['fixedFont']) {
$classes[] = 'text-monospace';
}
if ($config['enableTabulator']) {
$classes[] = 't3js-enable-tab';
}
$attributes['class'] = implode(' ', $classes);
$maximumHeight = (int)$backendUser->uc['resizeTextareas_MaxHeight'];
if ($maximumHeight > 0) {
// add the max-height from the users' preference to it
$attributes['style'] = 'max-height: ' . $maximumHeight . 'px';
}
if (isset($config['max']) && (int)$config['max'] > 0) {
$attributes['maxlength'] = (int)$config['max'];
}
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = htmlspecialchars(trim($config['placeholder']));
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<textarea ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . htmlspecialchars($itemValue) . '</textarea>';
$html[] = '</div>';
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-items-aside">';
$html[] = '<div class="btn-group">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,35 @@
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
/**
* Generation of TCEform elements of where the type is unknown
*/
class UnknownElement extends AbstractFormElement
{
/**
* Handler for unknown types.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$type = $this->data['parameterArray']['fieldConf']['config']['type'];
$renderType = $this->data['renderType'];
$resultArray = $this->initializeResultArray();
$resultArray['html'] = '<div class="alert alert-warning">Unknown type: <code>' . $type . '</code>' . ($renderType ? ', render type: <code>' . $renderType . '</code>' : '') . '</div>';
return $resultArray;
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Generation of elements of the type "user". This is a dummy implementation.
*
* type="user" elements should be combined with a custom renderType to create custom output.
* This implementation registered for type="user" kicks in if no renderType is given and is just
* a fallback implementation to hint developers that the TCA registration is incomplete.
*/
class UserElement extends AbstractFormElement
{
/**
* User defined field type
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
if (empty($parameterArray['fieldConf']['config']['userFunc'])) {
// If there is no (deprecated) userFunc, render some dummy output to explain this element
// should usually not be called at all.
// @deprecated The if can be removed in TYPO3 v10.0, keeping the body only.
$resultArray['html'] = '<div class="alert alert-warning">';
$resultArray['html'] .= 'This is dummy output: Field <code>' . htmlspecialchars($this->data['fieldName']) . '</code>';
$resultArray['html'] .= 'of table <code>' . htmlspecialchars($this->data['tableName']) . '</code>';
$resultArray['html'] .= ' is registered as type="user" element without a specific renderType.';
$resultArray['html'] .= ' Please look up details in TCA reference documentation for type="user".';
$resultArray['html'] .= '</div>';
return $resultArray;
}
// @deprecated since TYPO3 v9, everything below will be removed in TYPO3 v10.0.
trigger_error(
'Properties "userFunc", "noTableWrapping" and "parameters" will be removed in TYPO3 v10.0. Use a renderType instead.',
E_USER_DEPRECATED
);
$parameterArray['table'] = $this->data['tableName'];
$parameterArray['field'] = $this->data['fieldName'];
$parameterArray['row'] = $this->data['databaseRow'];
$parameterArray['parameters'] = $parameterArray['fieldConf']['config']['parameters'] ?? [];
$html = GeneralUtility::callUserFunction(
$parameterArray['fieldConf']['config']['userFunc'],
$parameterArray,
$this
);
if (!isset($parameterArray['fieldConf']['config']['noTableWrapping'])
|| (bool)$parameterArray['fieldConf']['config']['noTableWrapping'] === false
) {
$html = '<div class="formengine-field-item t3js-formengine-field-item">' . $html . '</div>';
}
$resultArray['html'] = $html;
return $resultArray;
}
}
@@ -0,0 +1,130 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Special type="user" element used in sys_file_storage is_public field
*
* @internal
*/
class UserSysFileStorageIsPublicElement extends AbstractFormElement
{
/**
* Default field information enabled for this element.
*
* @var array
*/
protected $defaultFieldInformation = [
'tcaDescription' => [
'renderType' => 'tcaDescription',
],
];
/**
* There are some edge cases where "is_public" can never be marked as true in the BE,
* for instance for storage located outside the document root or
* for storages driven by special driver such as Flickr, ...
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$isPublic = (bool)$GLOBALS['TCA']['sys_file_storage']['columns']['is_public']['config']['default'];
if ($this->data['command'] === 'edit') {
// Make sure the storage object can be retrieved which is not the case when new storage.
$lang = $this->getLanguageService();
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
try {
$storage = ResourceFactory::getInstance()->getStorageObject((int)$row['uid']);
$storageRecord = $storage->getStorageRecord();
$isPublic = $storage->isPublic() && $storageRecord['is_public'];
// Display a warning to the BE User in case settings is not inline with storage capability.
if ($storageRecord['is_public'] && !$storage->isPublic()) {
$message = GeneralUtility::makeInstance(
FlashMessage::class,
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.message.storage_is_no_public'),
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.header.storage_is_no_public'),
FlashMessage::WARNING
);
$defaultFlashMessageQueue->enqueue($message);
}
} catch (InvalidPathException $e) {
$message = GeneralUtility::makeInstance(
FlashMessage::class,
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:filestorage.invalidpathexception.message'),
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:filestorage.invalidpathexception.title'),
FlashMessage::ERROR
);
$defaultFlashMessageQueue->enqueue($message);
}
}
$isPublicAsString = $isPublic ? '1' : '0';
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($this->initializeResultArray(), $fieldInformationResult, false);
$checkboxParameters = $this->checkBoxParams($parameterArray['itemFormElName'], $isPublic, 0, 1, implode('', $parameterArray['fieldChangeFunc']));
$checkboxId = $parameterArray['itemFormElID'] . '_1';
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<div class="checkbox checkbox-type-toggle">';
$html[] = '<input type="checkbox"';
$html[] = ' class="checkbox-input"';
$html[] = ' value="1"';
$html[] = ' data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName'], ENT_QUOTES) . '"';
$html[] = ' id="' . htmlspecialchars($checkboxId, ENT_QUOTES) . '"';
$html[] = $checkboxParameters;
$html[] = $isPublic ? ' checked="checked"' : '';
$html[] = '/>';
$html[] = '<label class="checkbox-label" for="' . htmlspecialchars($checkboxId, ENT_QUOTES) . '">';
$html[] = '<span class="checkbox-label-text">' . $this->appendValueToLabelInDebugMode('&nbsp;', $isPublicAsString) . '</span>';
$html[] = '</label>';
$html[] = '<input type="hidden"';
$html[] = ' name="' . htmlspecialchars($parameterArray['itemFormElName'], ENT_QUOTES) . '"';
$html[] = ' value="' . $isPublicAsString . '"';
$html[] = ' />';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Backend\Form;
/*
* 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!
*/
/**
* Generic backend form exception
*/
class Exception extends \TYPO3\CMS\Backend\Exception
{
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
/**
* User is not allowed to edit this content elements
*/
class AccessDeniedContentEditException extends AccessDeniedException
{
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
/**
* User check did not survive "recordEditAccessInternals" to edit record
*/
class AccessDeniedEditInternalsException extends AccessDeniedException
{
}
@@ -0,0 +1,26 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\Exception;
/**
* Access denied exception.
* This indicated a recoverable error that should be changed to a user message.
* This abstract exception is extended by more fine grained exceptions.
*/
abstract class AccessDeniedException extends Exception
{
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
/**
* User access to record was denied by a makeEditForm_accessCheck hook
*/
class AccessDeniedHookException extends AccessDeniedException
{
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
/**
* User is not allowed to edit this page
*/
class AccessDeniedPageEditException extends AccessDeniedException
{
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
/**
* User is not allowed to add a new page
*/
class AccessDeniedPageNewException extends AccessDeniedException
{
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
/**
* User has no sufficient rights to modify a row that is located at root node
*/
class AccessDeniedRootNodeException extends AccessDeniedException
{
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
/**
* User has no modify table access to a table record
*/
class AccessDeniedTableModifyException extends AccessDeniedException
{
}
@@ -0,0 +1,24 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\Exception;
/**
* Thrown if a language overlay record is handled and no default language record is found.
*/
class DatabaseDefaultLanguageException extends Exception
{
}
@@ -0,0 +1,69 @@
<?php
namespace TYPO3\CMS\Backend\Form\Exception;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\Exception;
/**
* A record could not be fetched from database, maybe it vanished meanwhile.
*/
class DatabaseRecordException extends Exception
{
/**
* @var string Table name
*/
protected $tableName;
/**
* @var int Table row uid
*/
protected $uid;
/**
* Constructor overwrites default constructor.
*
* @param string $message Human readable error message
* @param int $code Exception code timestamp
* @param \Exception $previousException Possible exception from database layer
* @param string $tableName Table name query was working on
* @param int $uid Table row uid
*/
public function __construct($message, $code, \Exception $previousException = null, $tableName, $uid)
{
parent::__construct($message, $code, $previousException);
$this->tableName = $tableName;
$this->uid = $uid;
}
/**
* Return table name
*
* @return string
*/
public function getTableName()
{
return $this->tableName;
}
/**
* Return row uid
*
* @return int
*/
public function getUid()
{
return $this->uid;
}
}
@@ -0,0 +1,116 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldControl;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Renders the icon with link parameters to add a new record,
* typically used for single elements of type=group or type=select.
*/
class AddRecord extends AbstractNode
{
/**
* Add button control
*
* @return array As defined by FieldControl class
*/
public function render(): array
{
$options = $this->data['renderData']['fieldControlOptions'];
$parameterArray = $this->data['parameterArray'];
$itemName = $parameterArray['itemFormElName'];
// Handle options and fallback
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNew';
$setValue = $options['setValue'] ?? 'append';
$table = '';
if (isset($options['table'])) {
// Table given in options - use it
$table = $options['table'];
} elseif ($parameterArray['fieldConf']['config']['type'] === 'group'
&& isset($parameterArray['fieldConf']['config']['internal_type'])
&& $parameterArray['fieldConf']['config']['internal_type'] === 'db'
&& !empty($parameterArray['fieldConf']['config']['allowed'])
) {
// Use first table from allowed list if specific table is not set in options
$allowedTables = GeneralUtility::trimExplode(',', $parameterArray['fieldConf']['config']['allowed'], true);
$table = array_pop($allowedTables);
} elseif ($parameterArray['fieldConf']['config']['type'] === 'select'
&& !empty($parameterArray['fieldConf']['config']['foreign_table'])
) {
// Use foreign_table if given for type=select
$table = $parameterArray['fieldConf']['config']['foreign_table'];
}
if (empty($table)) {
// Still no table - this element can not handle the add control.
return [];
}
// Target pid of new records is current pid by default
$pid = $this->data['effectivePid'];
if (isset($options['pid'])) {
// pid configured in options - use it
$pid = $options['pid'];
} elseif (
isset($GLOBALS['TCA'][$table]['ctrl']['rootLevel'])
&& (int)$GLOBALS['TCA'][$table]['ctrl']['rootLevel'] === 1
) {
// Target table can only exist on root level - set 0 as pid
$pid = 0;
}
$prefixOfFormElName = 'data[' . $this->data['tableName'] . '][' . $this->data['databaseRow']['uid'] . '][' . $this->data['fieldName'] . ']';
$flexFormPath = '';
if (GeneralUtility::isFirstPartOfStr($itemName, $prefixOfFormElName)) {
$flexFormPath = str_replace('][', '/', substr($itemName, strlen($prefixOfFormElName) + 1, -1));
}
$urlParameters = [
'P' => [
'params' => [
'table' => $table,
'pid' => $pid,
'setValue' => $setValue,
],
'table' => $this->data['tableName'],
'field' => $this->data['fieldName'],
'uid' => $this->data['databaseRow']['uid'],
'flexFormPath' => $flexFormPath,
'returnUrl' => $this->data['returnUrl'],
],
];
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
return [
'iconIdentifier' => 'actions-add',
'title' => $title,
'linkAttributes' => [
'id' => htmlspecialchars($id),
'href' => (string)$uriBuilder->buildUriFromRoute('wizard_add', $urlParameters),
],
'requireJsModules' => [
['TYPO3/CMS/Backend/FormEngine/FieldControl/AddRecord' => 'function(FieldControl) {new FieldControl(' . GeneralUtility::quoteJSvalue('#' . $id) . ');}'],
],
];
}
}
@@ -0,0 +1,128 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldControl;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Renders the icon with link parameters to edit a selected element,
* typically used for single elements of type=group or type=select.
*/
class EditPopup extends AbstractNode
{
/**
* Edit popup control
*
* @return array As defined by FieldControl class
*/
public function render()
{
$languageService = $this->getLanguageService();
$options = $this->data['renderData']['fieldControlOptions'];
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.edit';
$parameterArray = $this->data['parameterArray'];
$itemName = $parameterArray['itemFormElName'];
$windowOpenParameters = $options['windowOpenParameters'] ?? 'height=800,width=600,status=0,menubar=0,scrollbars=1';
$flexFormDataStructureIdentifier = $this->data['flexFormDataStructureIdentifier'] ?? '';
$flexFormDataStructurePath = '';
if (!empty($flexFormDataStructureIdentifier)) {
if (empty($this->data['flexFormContainerName'])) {
// simple flex form element
$flexFormDataStructurePath = 'sheets/'
. $this->data['flexFormSheetName']
. '/ROOT/el/'
. $this->data['flexFormFieldName']
. '/TCEforms/config';
} else {
// flex form section container element
$flexFormDataStructurePath = 'sheets/'
. $this->data['flexFormSheetName']
. '/ROOT/el/'
. $this->data['flexFormFieldName']
. '/el/'
. $this->data['flexFormContainerName']
. '/el/'
. $this->data['flexFormContainerFieldName']
. '/TCEforms/config';
}
}
$urlParameters = [
'P' => [
'table' => $this->data['tableName'],
'field' => $this->data['fieldName'],
'formName' => 'editform',
'flexFormDataStructureIdentifier' => $flexFormDataStructureIdentifier,
'flexFormDataStructurePath' => $flexFormDataStructurePath,
'hmac' => GeneralUtility::hmac('editform' . $itemName, 'wizard_js'),
'fieldChangeFunc' => $parameterArray['fieldChangeFunc'],
'fieldChangeFuncHash' => GeneralUtility::hmac(serialize($parameterArray['fieldChangeFunc']), 'backend-link-browser'),
],
];
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
$url = (string)$uriBuilder->buildUriFromRoute('wizard_edit', $urlParameters);
$onClick = [];
$onClick[] = 'this.blur();';
$onClick[] = 'if (!TBE_EDITOR.curSelected(' . GeneralUtility::quoteJSvalue($itemName) . ')) {';
$onClick[] = 'top.TYPO3.Modal.confirm(';
$onClick[] = '"' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.header') . '",';
$onClick[] = '"' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.noSelItemForEdit') . '",';
$onClick[] = 'top.TYPO3.Severity.notice, [{text: TYPO3.lang[\'button.ok\'] || \'OK\', btnClass: \'btn-notice\', name: \'ok\'}]';
$onClick[] = ')';
$onClick[] = '.on("button.clicked", function(e) {';
$onClick[] = 'if (e.target.name == "ok") { top.TYPO3.Modal.dismiss(); }}';
$onClick[] = ');';
$onClick[] = 'return false;';
$onClick[] = '}';
$onClick[] = 'vHWin=window.open(';
$onClick[] = GeneralUtility::quoteJSvalue($url);
$onClick[] = '+\'&P[currentValue]=\'+TBE_EDITOR.rawurlencode(';
$onClick[] = 'document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
$onClick[] = ')';
$onClick[] = '+\'&P[currentSelectedValues]=\'+TBE_EDITOR.curSelected(';
$onClick[] = GeneralUtility::quoteJSvalue($itemName);
$onClick[] = '),';
$onClick[] = '\'\',';
$onClick[] = GeneralUtility::quoteJSvalue($windowOpenParameters);
$onClick[] = ');';
$onClick[] = 'vHWin.focus();';
$onClick[] = 'return false;';
return [
'iconIdentifier' => 'actions-open',
'title' => $title,
'linkAttributes' => [
'onClick' => implode('', $onClick),
],
];
}
/**
* Returns an instance of LanguageService
*
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldControl;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Renders the icon "select element via element browser",
* typically used for type=group.
*/
class ElementBrowser extends AbstractNode
{
/**
* Add button control
*
* @return array As defined by FieldControl class
*/
public function render()
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$elementName = $parameterArray['itemFormElName'];
$config = $parameterArray['fieldConf']['config'];
$internalType = (string)$config['internal_type'];
$allowed = $config['allowed'];
if (isset($config['readOnly']) && $config['readOnly']) {
return [];
}
if ($internalType === 'db') {
$title = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.browse_db';
} else {
$title = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.browse_file';
}
// Check against inline uniqueness - Create some onclick js for delete control and element browser
// to override record selection in some FAL scenarios - See 'appearance' docs of group element
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
$elementBrowserOnClickInline = '';
if ($this->data['isInlineChild']
&& $this->data['inlineParentUid']
&& $this->data['inlineParentConfig']['foreign_table'] === $table
&& $this->data['inlineParentConfig']['foreign_unique'] === $fieldName
) {
$objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']) . '-' . $table;
$elementBrowserOnClickInline = $objectPrefix . '|inline.checkUniqueElement|inline.setUniqueElement';
}
$elementBrowserType = $internalType;
if (is_array($config['appearance']) && isset($config['appearance']['elementBrowserType'])) {
$elementBrowserType = $config['appearance']['elementBrowserType'];
}
if (is_array($config['appearance']) && isset($config['appearance']['elementBrowserAllowed'])) {
$allowed = $config['appearance']['elementBrowserAllowed'];
}
// Remove any white-spaces from the allowed extension lists
$elementBrowserAllowed = implode(',', GeneralUtility::trimExplode(',', $allowed, true));
$elementBrowserOnClick = 'setFormValueOpenBrowser('
. GeneralUtility::quoteJSvalue($elementBrowserType) . ','
. GeneralUtility::quoteJSvalue($elementName . '|||' . $elementBrowserAllowed . '|' . $elementBrowserOnClickInline)
. ');'
. ' return false;';
return [
'iconIdentifier' => 'actions-insert-record',
'title' => $title,
'linkAttributes' => [
'onClick' => $elementBrowserOnClick,
],
];
}
}
@@ -0,0 +1,98 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldControl;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Renders the icon "insert record from clipboard",
* typically used for type=group.
*/
class InsertClipboard extends AbstractNode
{
/**
* Add button control
*
* @return array As defined by FieldControl class
*/
public function render(): array
{
$languageService = $this->getLanguageService();
$parameterArray = $this->data['parameterArray'];
$elementName = $parameterArray['itemFormElName'];
$config = $parameterArray['fieldConf']['config'];
$internalType = (string)$config['internal_type'];
$clipboardElements = $config['clipboardElements'];
if ((isset($config['readOnly']) && $config['readOnly'])
|| empty($clipboardElements)
) {
return [];
}
$title = '';
$dataAttributes = [
'element' => $elementName,
'clipboardItems' => [],
];
if ($internalType === 'file_reference' || $internalType === 'file') {
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Deprecation logged by TcaMigration class.
$title = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clipInsert_file'), count($clipboardElements));
foreach ($clipboardElements as $clipboardElement) {
$dataAttributes['clipboardItems'][] = [
'title' => rawurlencode(PathUtility::basename($clipboardElement['title'])),
'value' => $clipboardElement['value'],
];
}
} elseif ($internalType === 'db') {
$title = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clipInsert_db'), count($clipboardElements));
foreach ($clipboardElements as $clipboardElement) {
$dataAttributes['clipboardItems'][] = [
'title' => $clipboardElement['title'],
'value' => $clipboardElement['value'],
];
}
}
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
return [
'iconIdentifier' => 'actions-document-paste-into',
'title' => $title,
'linkAttributes' => [
'id' => htmlspecialchars($id),
'data-element' => $dataAttributes['element'],
'data-clipboard-items' => json_encode($dataAttributes['clipboardItems']),
],
'requireJsModules' => [
['TYPO3/CMS/Backend/FormEngine/FieldControl/InsertClipboard' => 'function(FieldControl) {new FieldControl(' . GeneralUtility::quoteJSvalue('#' . $id) . ');}'],
],
];
}
/**
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldControl;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Renders the icon with link parameters to open the element browser.
* Used in InputLinkElement.
*/
class LinkPopup extends AbstractNode
{
/**
* Link popup control
*
* @return array As defined by FieldControl class
*/
public function render(): array
{
$options = $this->data['renderData']['fieldControlOptions'];
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.link';
$parameterArray = $this->data['parameterArray'];
$itemName = $parameterArray['itemFormElName'];
$windowOpenParameters = $options['windowOpenParameters'] ?? 'height=800,width=1000,status=0,menubar=0,scrollbars=1';
$linkBrowserArguments = [];
if (isset($options['blindLinkOptions'])) {
$linkBrowserArguments['blindLinkOptions'] = $options['blindLinkOptions'];
}
if (isset($options['blindLinkFields'])) {
$linkBrowserArguments['blindLinkFields'] = $options['blindLinkFields'];
}
if (isset($options['allowedExtensions'])) {
$linkBrowserArguments['allowedExtensions'] = $options['allowedExtensions'];
}
$urlParameters = [
'P' => [
'params' => $linkBrowserArguments,
'table' => $this->data['tableName'],
'uid' => $this->data['databaseRow']['uid'],
'pid' => $this->data['databaseRow']['pid'],
'field' => $this->data['fieldName'],
'formName' => 'editform',
'itemName' => $itemName,
'hmac' => GeneralUtility::hmac('editform' . $itemName, 'wizard_js'),
'fieldChangeFunc' => $parameterArray['fieldChangeFunc'],
'fieldChangeFuncHash' => GeneralUtility::hmac(serialize($parameterArray['fieldChangeFunc']), 'backend-link-browser'),
],
];
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
$url = (string)$uriBuilder->buildUriFromRoute('wizard_link', $urlParameters);
$onClick = [];
$onClick[] = 'this.blur();';
$onClick[] = 'vHWin=window.open(';
$onClick[] = GeneralUtility::quoteJSvalue($url);
$onClick[] = '+\'&P[currentValue]=\'+TBE_EDITOR.rawurlencode(';
$onClick[] = 'document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
$onClick[] = ')';
$onClick[] = '+\'&P[currentSelectedValues]=\'+TBE_EDITOR.curSelected(';
$onClick[] = GeneralUtility::quoteJSvalue($itemName);
$onClick[] = '),';
$onClick[] = '\'\',';
$onClick[] = GeneralUtility::quoteJSvalue($windowOpenParameters);
$onClick[] = ');';
$onClick[] = 'vHWin.focus();';
$onClick[] = 'return false;';
return [
'iconIdentifier' => 'actions-wizard-link',
'title' => $title,
'linkAttributes' => [
'onClick' => implode('', $onClick),
],
];
}
}
@@ -0,0 +1,103 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldControl;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Renders the icon with link parameters to jump to the list module
* "single table" view, showing only one configurable table.
*/
class ListModule extends AbstractNode
{
/**
* Add button control
*
* @return array As defined by FieldControl class
*/
public function render()
{
$options = $this->data['renderData']['fieldControlOptions'];
$parameterArray = $this->data['parameterArray'];
// Handle options and fallback
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.list';
$table = '';
if (isset($options['table'])) {
// Table given in options - use it
$table = $options['table'];
} elseif ($parameterArray['fieldConf']['config']['type'] === 'group'
&& isset($parameterArray['fieldConf']['config']['internal_type'])
&& $parameterArray['fieldConf']['config']['internal_type'] === 'db'
&& !empty($parameterArray['fieldConf']['config']['allowed'])
) {
// Use first table from allowed list if specific table is not set in options
$allowedTables = GeneralUtility::trimExplode(',', $parameterArray['fieldConf']['config']['allowed'], true);
$table = array_pop($allowedTables);
} elseif ($parameterArray['fieldConf']['config']['type'] === 'select'
&& !empty($parameterArray['fieldConf']['config']['foreign_table'])
) {
// Use foreign_table if given for type=select
$table = $parameterArray['fieldConf']['config']['foreign_table'];
}
if (empty($table)) {
// Still no table - this element can not handle the list control.
return [];
}
// Target pid of new records is current pid by default
$pid = $this->data['effectivePid'];
if (isset($options['pid'])) {
// pid configured in options - use it
$pid = $options['pid'];
} elseif (isset($GLOBALS['TCA'][$table]['ctrl']['rootLevel']) && (int)$GLOBALS['TCA'][$table]['ctrl'] === 1) {
// Target table can only exist on root level - set 0 as pid
$pid = 0;
}
$urlParameters = [
'P' => [
'params' => [
'table' => $table,
'pid' => $pid,
],
'table' => $this->data['tableName'],
'uid' => $this->data['databaseRow']['uid'],
'returnUrl' => $this->data['returnUrl'],
],
];
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
return [
'iconIdentifier' => 'actions-system-list-open',
'title' => $title,
'linkAttributes' => [
'id' => htmlspecialchars($id),
'href' => (string)$uriBuilder->buildUriFromRoute('wizard_list', $urlParameters),
],
'requireJsModules' => [
['TYPO3/CMS/Backend/FormEngine/FieldControl/ListModule' => 'function(FieldControl) {new FieldControl(' . GeneralUtility::quoteJSvalue('#' . $id) . ');}'],
],
];
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldControl;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* "Reset selection to previous selected items" icon,
* typically used by type=select with renderType=selectSingleBox
*/
class ResetSelection extends AbstractNode
{
/**
* Add button control
*
* @return array As defined by FieldControl class
*/
public function render()
{
$parameterArray = $this->data['parameterArray'];
$itemName = $parameterArray['itemFormElName'];
$selectItems = $parameterArray['fieldConf']['config']['items'];
$itemArray = array_flip($parameterArray['itemFormElValue']);
$initiallySelectedIndices = [];
foreach ($selectItems as $i => $item) {
$value = $item[1];
// Selected or not by default
if (isset($itemArray[$value])) {
$initiallySelectedIndices[] = $i;
}
}
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
return [
'iconIdentifier' => 'actions-edit-undo',
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.revertSelection',
'linkAttributes' => [
'id' => htmlspecialchars($id),
'data-item-name' => htmlspecialchars($itemName),
'data-selected-indices' => json_encode($initiallySelectedIndices),
],
'requireJsModules' => [
['TYPO3/CMS/Backend/FormEngine/FieldControl/ResetSelection' => 'function(FieldControl) {new FieldControl(' . GeneralUtility::quoteJSvalue('#' . $id) . ');}'],
],
];
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldControl;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Renders the icon with link parameters to the table wizard,
* typically used for type=text with renderType=textTable.
*/
class TableWizard extends AbstractNode
{
/**
* Add button control
*
* @return array As defined by FieldControl class
*/
public function render()
{
$options = $this->data['renderData']['fieldControlOptions'];
$parameterArray = $this->data['parameterArray'];
$itemName = $parameterArray['itemFormElName'];
$row = $this->data['databaseRow'];
if (!MathUtility::canBeInterpretedAsInteger($row['uid'])) {
return [];
}
// Handle options and fallback
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.tableWizard';
$xmlOutput = isset($options['xmlOutput']) ? (int)$options['xmlOutput'] : 0;
$numNewRows = isset($options['numNewRows']) ? (int)$options['numNewRows'] : 5;
$prefixOfFormElName = 'data[' . $this->data['tableName'] . '][' . $this->data['databaseRow']['uid'] . '][' . $this->data['fieldName'] . ']';
$flexFormPath = '';
if (GeneralUtility::isFirstPartOfStr($itemName, $prefixOfFormElName)) {
$flexFormPath = str_replace('][', '/', substr($itemName, strlen($prefixOfFormElName) + 1, -1));
}
$urlParameters = [
'P' => [
'params' => [
'xmlOutput' => $xmlOutput,
'numNewRows' => $numNewRows,
],
'table' => $this->data['tableName'],
'field' => $this->data['fieldName'],
'uid' => $this->data['databaseRow']['uid'],
'flexFormPath' => $flexFormPath,
'returnUrl' => $this->data['returnUrl'],
],
];
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
return [
'iconIdentifier' => 'content-table',
'title' => $title,
'linkAttributes' => [
'id' => htmlspecialchars($id),
'href' => (string)$uriBuilder->buildUriFromRoute('wizard_table', $urlParameters),
],
'requireJsModules' => [
['TYPO3/CMS/Backend/FormEngine/FieldControl/TableWizard' => 'function(FieldControl) {new FieldControl(' . GeneralUtility::quoteJSvalue('#' . $id) . ');}'],
],
];
}
}
@@ -0,0 +1,97 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldInformation;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* This field information node is used in be_user admin field
* to show some additional information if the edited user
* is a system maintainer or becomes one if togging the admin flag.
*/
class AdminIsSystemMaintainer extends AbstractNode
{
/**
* Handler for single nodes
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws \RuntimeException
*/
public function render(): array
{
if ($this->data['tableName'] !== 'be_users' || $this->data['fieldName'] !== 'admin') {
throw new \RuntimeException(
'The adminIsSystemMaintainer field information can be used for admin field in be_users table only',
1537273550
);
}
$resultArray = $this->initializeResultArray();
if ($this->data['command'] === 'new') {
// Early return on 'new' records - nothing we can do here
return $resultArray;
}
// False if current user is not in system maintainer list or if switch to user mode is active
$isCurrentUserSystemMaintainer = $this->getBackendUser()->isSystemMaintainer();
$systemMaintainers = array_map('intval', $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
$isTargetUserInSystemMaintainerList = in_array((int)$this->data['vanillaUid'], $systemMaintainers, true);
if ($isTargetUserInSystemMaintainerList) {
$languageService = $this->getLanguageService();
$isTargetUserAdmin = (int)$this->data['databaseRow']['admin'] === 1;
if ($isCurrentUserSystemMaintainer) {
if ($isTargetUserAdmin) {
// User is a system maintainer
$fieldInformationText = '<strong>' . htmlspecialchars($languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:formEngine.beUser.admin.information.userIsSystemMaintainer'
)) . '</strong>';
} else {
// User is currently not an admin, but set as system maintainer (in-effective).
// If admin field is set to 1, the user is therefore system maintainer again.
$fieldInformationText = '<strong>' . htmlspecialchars($languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:formEngine.beUser.admin.information.userWillBecomeSystemMaintainer'
)) . '</strong>';
}
} else {
// User is in system maintainer list, user can not change admin and password
$fieldInformationText = '<strong>' . htmlspecialchars($languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:formEngine.beUser.admin.information.userAdminAndPasswordChangeNotAllowed'
)) . '</strong>';
}
$resultArray['html'] = $fieldInformationText;
}
return $resultArray;
}
/**
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldInformation;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* Render localized ['columns']['theField']['description'] text as default
* field information node. This is typically displayed in elements below the
* element label and the field content.
*/
class TcaDescription extends AbstractNode
{
/**
* Handler for single nodes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
if (!empty($this->data['parameterArray']['fieldConf']['description'])) {
$fieldInformationText = $this->getLanguageService()->sL($this->data['parameterArray']['fieldConf']['description']);
if (trim($fieldInformationText) !== '') {
$resultArray['html'] = '<span class="formengine-field-item-description text-muted">' . htmlspecialchars($fieldInformationText) . '</span>';
}
}
return $resultArray;
}
/**
* Returns the LanguageService
*
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,94 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldWizard;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\DiffUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Renders the diff-view of default language record content compared with what the record was originally
* translated from. Will render content if any is found in the internal array.
*
* This is typically used of renderTypes that are based on text input
*/
class DefaultLanguageDifferences extends AbstractNode
{
/**
* Render the diff view if enabled
*
* @return array Result array
*/
public function render(): array
{
$result = $this->initializeResultArray();
$row = $this->data['databaseRow'];
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$fieldConfig = $this->data['processedTca']['columns'][$fieldName];
$l10nDisplay = $this->data['parameterArray']['fieldConf']['l10n_display'] ?? '';
$defaultLanguageRow = $this->data['defaultLanguageRow'] ?? null;
$defaultLanguageDiffRow = $this->data['defaultLanguageDiffRow'][$table . ':' . $row['uid']] ?? null;
if (!is_array($defaultLanguageDiffRow)
|| GeneralUtility::inList($l10nDisplay, 'hideDiff')
|| GeneralUtility::inList($l10nDisplay, 'defaultAsReadonly')
|| !isset($defaultLanguageDiffRow[$fieldName])
|| $fieldConfig['config']['type'] === 'inline'
|| $fieldConfig['config']['type'] === 'flex'
) {
// Early return if there is no diff row or if display is disabled
return $result;
}
$languageService = $this->getLanguageService();
$html = [];
if ((string)$defaultLanguageDiffRow[$fieldName] !== (string)$defaultLanguageRow[$fieldName]) {
// Create diff-result:
$diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
$diffUtility->stripTags = false;
$diffResult = $diffUtility->makeDiffDisplay(
BackendUtility::getProcessedValue($table, $fieldName, $defaultLanguageDiffRow[$fieldName], 0, 1),
BackendUtility::getProcessedValue($table, $fieldName, $defaultLanguageRow[$fieldName], 0, 1)
);
$html[] = '<div class="t3-form-original-language-diff">';
$html[] = '<div class="t3-form-original-language-diffheader">';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.changeInOrig'));
$html[] = '</div>';
$html[] = '<div class="t3-form-original-language-diffcontent">';
$html[] = '<div class="diff">';
$html[] = '<div class="diff-item">';
$html[] = '<div class="diff-item-result diff-item-result-inline">' . $diffResult . '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
}
$result['html'] = implode(LF, $html);
return $result;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldWizard;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Render thumbnails of selected files,
* typically used with type=group and internal_type=file and file_reference.
*/
class FileThumbnails extends AbstractNode
{
/**
* Render thumbnails of selected files
*
* @return array
*/
public function render(): array
{
$result = $this->initializeResultArray();
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$selectedItems = $parameterArray['itemFormElValue'];
if (!isset($config['internal_type'])
|| ($config['internal_type'] !== 'file' && $config['internal_type'] !== 'file_reference')
) {
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Deprecation logged by TcaMigration class.
// Thumbnails only make sense on file and file_reference
return $result;
}
$fileFactory = ResourceFactory::getInstance();
$thumbnailsHtml = [];
foreach ($selectedItems as $selectedItem) {
$uidOrPath = $selectedItem['uidOrPath'];
if (MathUtility::canBeInterpretedAsInteger($uidOrPath)) {
$fileObject = $fileFactory->getFileObject($uidOrPath);
if (!$fileObject->isMissing()) {
$extension = $fileObject->getExtension();
if (GeneralUtility::inList(
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'],
$extension
)
) {
$thumbnailsHtml[] = '<li>';
$thumbnailsHtml[] = '<span class="thumbnail">';
$thumbnailsHtml[] = $fileObject->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, [])->getPublicUrl(true);
$thumbnailsHtml[] = '</span>';
$thumbnailsHtml[] = '</li>';
}
}
} else {
$rowCopy = [];
$rowCopy[$fieldName] = $uidOrPath;
try {
$icon = BackendUtility::thumbCode(
$rowCopy,
$table,
$fieldName,
'',
'',
$config['uploadfolder'],
0,
' align="middle"'
);
$thumbnailsHtml[] =
'<li>'
. '<span class="thumbnail">'
. $icon
. '</span>'
. '</li>';
} catch (\Exception $exception) {
$message = $exception->getMessage();
$flashMessage = GeneralUtility::makeInstance(
FlashMessage::class,
$message,
'',
FlashMessage::ERROR,
true
);
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
$this->logger->warning($message, ['table' => $table, 'row' => $row]);
}
}
}
$html = [];
if (!empty($thumbnailsHtml)) {
$html[] = '<ul class="list-inline">';
$html[] = implode(LF, $thumbnailsHtml);
$html[] = '</ul>';
}
$result['html'] = implode(LF, $html);
return $result;
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldWizard;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Render list of allowed / disalowwed file types,
* typically used with type=group and internal_type=file.
*/
class FileTypeList extends AbstractNode
{
/**
* Render list of allowed and disallowed file types
*
* @return array
*/
public function render(): array
{
$result = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
if (!isset($config['allowed']) || !is_string($config['allowed']) || empty($config['allowed'])
|| !isset($config['internal_type']) || $config['internal_type'] !== 'file'
) {
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Deprecation logged by TcaMigration class.
// No handling if the field has no, or funny "allowed" setting, and if internal_type is not "file"
return $result;
}
$allowed = GeneralUtility::trimExplode(',', $config['allowed'], true);
$disallowed = isset($config['disallowed']) ? GeneralUtility::trimExplode(',', $config['disallowed'], true) : [];
$allowedHtml = [];
foreach ($allowed as $item) {
$allowedHtml[] = '<span class="label label-success">' . htmlspecialchars(strtoupper($item)) . '</span> ';
}
$disallowedHtml = [];
foreach ($disallowed as $item) {
$disallowedHtml[] = '<span class="label label-danger">' . htmlspecialchars(strtoupper($item)) . '</span> ';
}
$html = [];
if (!empty($allowedHtml) || !empty($disallowedHtml)) {
$html[] = '<div class="help-block">';
$html[] = implode(LF, $allowedHtml);
$html[] = implode(LF, $disallowedHtml);
$html[] = '</div>';
}
$result['html'] = implode(LF, $html);
return $result;
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldWizard;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Render file upload,
* typically used with type=group and internal_type=file and file_reference.
*/
class FileUpload extends AbstractNode
{
/**
* Render file upload
*
* @return array
*/
public function render(): array
{
$backendUser = $this->getBackendUserAuthentication();
$result = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$itemName = $parameterArray['itemFormElName'];
$uploadFieldId = $parameterArray['itemFormElID'] . '_files';
$config = $parameterArray['fieldConf']['config'];
$maxItems = $config['maxitems'];
$isDirectFileUploadEnabled = (bool)$backendUser->uc['edit_docModuleUpload'];
if (!isset($config['internal_type'])
|| $config['internal_type'] !== 'file'
|| !$isDirectFileUploadEnabled
|| empty($config['uploadfolder'])
) {
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Deprecation logged by TcaMigration class.
// No upload if disabled for user or upload folder missing
return $result;
}
if ($maxItems === 1) {
// If maxItems = 1 then automatically replace the current item file selector
$resultArray['additionalJavaScriptPost'][] =
'TBE_EDITOR.clearBeforeSettingFormValueFromBrowseWin[' . GeneralUtility::quoteJSvalue($itemName) . '] = {'
. 'itemFormElID_file: ' . GeneralUtility::quoteJSvalue($uploadFieldId)
. '}';
}
// Insert the multiple attribute to enable HTML5 multiple file upload
$selectorMultipleAttribute = '';
$multipleFilenameSuffix = '';
if ($maxItems > 1) {
$selectorMultipleAttribute = ' multiple="multiple"';
$multipleFilenameSuffix = '[]';
}
$html= [];
$html[] = '<div id="' . $uploadFieldId . '">';
$html[] = '<input';
$html[] = ' type="file"';
$html[] = $selectorMultipleAttribute;
$html[] = ' name="data_files' . $this->data['elementBaseName'] . $multipleFilenameSuffix . '"';
$html[] = ' size="35"';
$html[] = ' onchange="' . implode('', $parameterArray['fieldChangeFunc']) . '"';
$html[] = '/>';
$html[] = '</div>';
$result['html'] = implode(LF, $html);
return $result;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,141 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldWizard;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\DataHandling\Localization\State;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* Allows to define the localization state per field.
*/
class LocalizationStateSelector extends AbstractNode
{
/**
* Render the radio buttons if enabled
*
* @return array Result array
*/
public function render(): array
{
$languageService = $this->getLanguageService();
$result = $this->initializeResultArray();
$fieldName = $this->data['fieldName'];
$l10nStateFieldName = '';
if (isset($l10nStateFieldName)) {
$l10nStateFieldName = 'l10n_state';
}
if (
!$l10nStateFieldName
|| !isset($this->data['defaultLanguageRow'])
|| !isset($this->data['processedTca']['columns'][$fieldName]['config']['behaviour']['allowLanguageSynchronization'])
|| !$this->data['processedTca']['columns'][$fieldName]['config']['behaviour']['allowLanguageSynchronization']
) {
return $result;
}
$l10nParentFieldName = $this->data['processedTca']['ctrl']['transOrigPointerField'] ?? null;
$l10nSourceFieldName = $this->data['processedTca']['ctrl']['translationSource'] ?? null;
$sourceLanguageTitle = '';
$fieldValueInParentRow = '';
$fieldValueInSourceRow = null;
if ($l10nParentFieldName && $this->data['databaseRow'][$l10nParentFieldName] > 0) {
if ($l10nSourceFieldName && $this->data['databaseRow'][$l10nSourceFieldName] > 0) {
$languageField = $this->data['processedTca']['ctrl']['languageField'] ?? null;
if ($languageField
&& isset($this->data['sourceLanguageRow'][$languageField])
&& $this->data['sourceLanguageRow'][$languageField] > 0
) {
$languageUidOfSourceRow = $this->data['sourceLanguageRow'][$languageField];
$sourceLanguageTitle = $this->data['systemLanguageRows'][$languageUidOfSourceRow]['title'] ?? '';
$fieldValueInSourceRow = $this->data['sourceLanguageRow'][$fieldName] ?? null;
}
}
$fieldValueInParentRow = (string)$this->data['defaultLanguageRow'][$fieldName];
}
$localizationState = State::fromJSON(
$this->data['tableName'],
$this->data['databaseRow'][$l10nStateFieldName] ?? null
);
$fieldElementName = 'data[' . htmlspecialchars($this->data['tableName']) . ']'
. '[' . htmlspecialchars((string)$this->data['databaseRow']['uid']) . ']'
. '[' . htmlspecialchars($l10nStateFieldName) . ']'
. '[' . htmlspecialchars($this->data['fieldName']) . ']';
$html = [];
$html[] = '<div class="t3js-l10n-state-container">';
$html[] = '<div>';
$html[] = '<strong>';
$html[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.header');
$html[] = '</strong>';
$html[] = '</div>';
$html[] = '<div class="radio radio-inline">';
$html[] = '<label>';
$html[] = '<input';
$html[] = ' type="radio"';
$html[] = ' name="' . htmlspecialchars($fieldElementName) . '"';
$html[] = ' class="t3js-l10n-state-custom"';
$html[] = ' value="custom"';
$html[] = $localizationState->isCustomState($fieldName) ? ' checked="checked"' : '';
$html[] = ' data-original-language-value=""';
$html[] = '>';
$html[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.customValue');
$html[] = '</label>';
$html[] = '</div>';
$html[] = '<div class="radio radio-inline">';
$html[] = '<label>';
$html[] = '<input';
$html[] = ' type="radio"';
$html[] = ' name="' . htmlspecialchars($fieldElementName) . '"';
$html[] = ' value="parent"';
$html[] = $localizationState->isParentState($fieldName) ? ' checked="checked"' : '';
$html[] = ' data-original-language-value="' . htmlspecialchars((string)$fieldValueInParentRow) . '"';
$html[] = '>';
$html[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.defaultLanguageValue');
$html[] = '</label>';
$html[] = '</div>';
if ($fieldValueInSourceRow !== null) {
$html[] = '<div class="radio radio-inline">';
$html[] = '<label>';
$html[] = '<input';
$html[] = ' type="radio"';
$html[] = ' name="' . htmlspecialchars($fieldElementName) . '"';
$html[] = ' value="source"';
$html[] = $localizationState->isSourceState($fieldName) ? ' checked="checked"' : '';
$html[] = ' data-original-language-value="' . htmlspecialchars((string)$fieldValueInSourceRow) . '"';
$html[] = '>';
$html[] = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.sourceLanguageValue'), htmlspecialchars($sourceLanguageTitle));
$html[] = '</label>';
$html[] = '</div>';
}
$html[] = '</div>';
$result['html'] = implode(LF, $html);
return $result;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,154 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldWizard;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Render values of "other" languages. If editing a localized row, this is typically
* the content value of the according default record, but it may render field values
* of other languages too, depending on configuration.
*/
class OtherLanguageContent extends AbstractNode
{
/**
* Render other language content if enabled.
*
* @return array
*/
public function render(): array
{
$result = $this->initializeResultArray();
$fieldName = $this->data['fieldName'];
$fieldConfig = $this->data['processedTca']['columns'][$fieldName];
$l10nDisplay = $this->data['parameterArray']['fieldConf']['l10n_display'] ?? '';
$defaultLanguageRow = $this->data['defaultLanguageRow'] ?? null;
if (!is_array($defaultLanguageRow)
|| GeneralUtility::inList($l10nDisplay, 'hideDiff')
|| GeneralUtility::inList($l10nDisplay, 'defaultAsReadonly')
|| $fieldConfig['config']['type'] === 'inline'
|| $fieldConfig['config']['type'] === 'flex'
|| ($fieldConfig['config']['type'] === 'group' && isset($fieldConfig['config']['MM']))
|| ($fieldConfig['config']['type'] === 'select' && isset($fieldConfig['config']['MM']))
) {
// Early return if there is no default language row or the display is disabled
return $result;
}
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$table = $this->data['tableName'];
$html = [];
$defaultLanguageValue = BackendUtility::getProcessedValue(
$table,
$fieldName,
$defaultLanguageRow[$fieldName],
0,
true,
false,
$defaultLanguageRow['uid'],
true,
$defaultLanguageRow['pid']
);
if ($defaultLanguageValue !== '') {
$iconIdentifier = $this->data['systemLanguageRows'][0]['flagIconIdentifier'] ?: 'flags-multiple';
$html[] = '<div class="t3-form-original-language">';
$html[] = $iconFactory->getIcon($iconIdentifier, Icon::SIZE_SMALL)->render();
$html[] = $this->previewFieldValue($defaultLanguageValue, $fieldConfig, $fieldName);
$html[] = '</div>';
}
$additionalPreviewLanguages = $this->data['additionalLanguageRows'];
foreach ($additionalPreviewLanguages as $previewLanguage) {
$defaultLanguageValue = BackendUtility::getProcessedValue(
$table,
$fieldName,
$previewLanguage[$fieldName],
0,
true
);
if ($defaultLanguageValue !== '') {
$html[] = '<div class="t3-form-original-language">';
$html[] = $iconFactory->getIcon($this->data['systemLanguageRows'][$previewLanguage['sys_language_uid']]['flagIconIdentifier'], Icon::SIZE_SMALL)->render();
$html[] = $this->previewFieldValue($defaultLanguageValue, $fieldConfig, $fieldName);
$html[] = '</div>';
}
}
$result['html'] = implode(LF, $html);
return $result;
}
/**
* Rendering preview output of a field value which is not shown as a form field but just outputted.
*
* @param string $value The value to output
* @param array $config Configuration for field.
* @param string $field Name of field.
* @return string HTML formatted output
*/
protected function previewFieldValue($value, $config, $field = '')
{
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Deprecation logged by TcaMigration class.
$value = (string)$value;
if ($config['config']['type'] === 'group'
&& ($config['config']['internal_type'] === 'file' || $config['config']['internal_type'] === 'file_reference')
) {
// Ignore upload folder if internal_type is file_reference
if ($config['config']['internal_type'] === 'file_reference') {
$config['config']['uploadfolder'] = '';
}
$table = 'tt_content';
// Making the array of file items:
$itemArray = GeneralUtility::trimExplode(',', $value, true);
// Showing thumbnails:
$imgs = [];
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
foreach ($itemArray as $imgRead) {
$imgParts = explode('|', $imgRead);
$imgPath = rawurldecode($imgParts[0]);
$rowCopy = [];
$rowCopy[$field] = $imgPath;
// Icon + click menu:
$absFilePath = GeneralUtility::getFileAbsFileName($config['config']['uploadfolder'] ? $config['config']['uploadfolder'] . '/' . $imgPath : $imgPath);
$fileInformation = pathinfo($imgPath);
$title = $fileInformation['basename'] . ($absFilePath && @is_file($absFilePath))
? ' (' . GeneralUtility::formatSize(filesize($absFilePath)) . ')'
: ' - FILE NOT FOUND!';
$fileIcon = '<span title="' . htmlspecialchars($title) . '">' . $iconFactory->getIconForFileExtension($fileInformation['extension'], Icon::SIZE_SMALL)->render() . '</span>';
$imgs[] =
'<span class="text-nowrap">' .
BackendUtility::thumbCode(
$rowCopy,
$table,
$field,
'',
'',
$config['config']['uploadfolder'],
0,
' align="middle"'
) .
($absFilePath ? BackendUtility::wrapClickMenuOnIcon($fileIcon, $absFilePath) : $fileIcon) .
$imgPath .
'</span>';
}
return implode('<br />', $imgs);
}
return nl2br(htmlspecialchars($value));
}
}
@@ -0,0 +1,114 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldWizard;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Render details of selected records,
* typically used with type=group and internal_type=db.
*/
class RecordsOverview extends AbstractNode
{
/**
* Render table with record details
*
* @return array
*/
public function render(): array
{
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUserAuthentication();
$result = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$selectedItems = $parameterArray['itemFormElValue'];
$maxTitleLength = $backendUser->uc['titleLen'];
if (!isset($config['internal_type']) || $config['internal_type'] !== 'db') {
// Table list makes sense on db only
return $result;
}
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$recordsOverviewHtml = [];
foreach ($selectedItems as $selectedItem) {
$title = (string)$selectedItem['title'];
if (empty($title)) {
$title = '[' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']';
}
$shortenedTitle = GeneralUtility::fixed_lgd_cs($title, $maxTitleLength);
$linkedIcon = BackendUtility::wrapClickMenuOnIcon(
$iconFactory->getIconForRecord($selectedItem['table'], $selectedItem['row'], Icon::SIZE_SMALL)->render(),
$selectedItem['table'],
$selectedItem['uid']
);
$linkedTitle = BackendUtility::wrapClickMenuOnIcon(
htmlspecialchars($shortenedTitle),
$selectedItem['table'],
$selectedItem['uid']
);
$recordsOverviewHtml[] = '<tr>';
$recordsOverviewHtml[] = '<td class="col-icon">';
$recordsOverviewHtml[] = $linkedIcon;
$recordsOverviewHtml[] = '</td>';
$recordsOverviewHtml[] = '<td class="col-title">';
$recordsOverviewHtml[] = $linkedTitle;
$recordsOverviewHtml[] = '<span class="text-muted">';
$recordsOverviewHtml[] = ' [' . $selectedItem['uid'] . ']';
$recordsOverviewHtml[] = '</span>';
$recordsOverviewHtml[] = '</td>';
$recordsOverviewHtml[] = '</tr>';
}
$html = [];
if (!empty($recordsOverviewHtml)) {
$html[] = '<div class="table-fit">';
$html[] = '<table class="table table-white">';
$html[] = '<tbody>';
$html[] = implode(LF, $recordsOverviewHtml);
$html[] = '</tbody>';
$html[] = '</table>';
$html[] = '</div>';
}
$result['html'] = implode(LF, $html);
return $result;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldWizard;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
/**
* Render thumbnails of icons,
* typically used with type=select.
*/
class SelectIcons extends AbstractNode
{
/**
* Render thumbnails of selected files
*
* @return array
*/
public function render(): array
{
$result = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$selectItems = $parameterArray['fieldConf']['config']['items'];
$selectItemCounter = 0;
foreach ($selectItems as $item) {
if ($item[1] === '--div--') {
continue;
}
$icon = !empty($item[2]) ? FormEngineUtility::getIconHtml($item[2], $item[0], $item[0]) : '';
if ($icon) {
$fieldValue = $this->data['databaseRow'][$this->data['fieldName']];
$selectIcons[] = [
'title' => $item[0],
'active' => ($fieldValue[0] === (string)$item[1]) ? true : false,
'icon' => $icon,
'index' => $selectItemCounter,
];
}
$selectItemCounter++;
}
$html = [];
if (!empty($selectIcons)) {
$html[] = '<div class="t3js-forms-select-single-icons icon-list">';
$html[] = '<div class="row">';
foreach ($selectIcons as $i => $selectIcon) {
$active = $selectIcon['active'] ? ' active' : '';
$html[] = '<div class="item' . $active . '">';
if (is_array($selectIcon)) {
$html[] = '<a href="#" title="' . htmlspecialchars($selectIcon['title'], ENT_COMPAT, 'UTF-8', false) . '" data-select-index="' . htmlspecialchars((string)$selectIcon['index']) . '">';
$html[] = $selectIcon['icon'];
$html[] = '</a>';
}
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
}
$result['html'] = implode(LF, $html);
return $result;
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FieldWizard;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Render list of tables and link to element browser,
* typically used with type=group and internal_type=db.
*/
class TableList extends AbstractNode
{
/**
* Render table buttons
*
* @return array
*/
public function render(): array
{
$languageService = $this->getLanguageService();
$result = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$itemName = $parameterArray['itemFormElName'];
if (!isset($config['allowed']) || !is_string($config['allowed']) || empty($config['allowed'])
|| !isset($config['internal_type']) || $config['internal_type'] !== 'db'
) {
// No handling if the field has no, or funny "allowed" setting, and if internal_type is not "db"
return $result;
}
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$allowed = GeneralUtility::trimExplode(',', $config['allowed'], true);
$allowedTablesHtml = [];
foreach ($allowed as $tableName) {
if ($tableName === '*') {
$label = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.allTables');
$allowedTablesHtml[] = '<span>';
$allowedTablesHtml[] = htmlspecialchars($label);
$allowedTablesHtml[] = '</span>';
} else {
$label = $languageService->sL($GLOBALS['TCA'][$tableName]['ctrl']['title']);
$icon = $iconFactory->getIconForRecord($tableName, [], Icon::SIZE_SMALL)->render();
if ((bool)($config['fieldControl']['elementBrowser']['disabled'] ?? false)) {
$allowedTablesHtml[] = '<span class="tablelist-item-nolink">';
$allowedTablesHtml[] = $icon;
$allowedTablesHtml[] = htmlspecialchars($label);
$allowedTablesHtml[] = '</span>';
} else {
$onClick = [];
$onClick[] = 'setFormValueOpenBrowser(';
$onClick[] = '\'db\',';
$onClick[] = GeneralUtility::quoteJSvalue($itemName . '|||' . $tableName);
$onClick[] = ');';
$onClick[] = 'return false;';
$allowedTablesHtml[] = '<a href="#" onClick="' . htmlspecialchars(implode('', $onClick)) . '" class="btn btn-default">';
$allowedTablesHtml[] = $icon;
$allowedTablesHtml[] = htmlspecialchars($label);
$allowedTablesHtml[] = '</a>';
}
}
}
$html = [];
$html[] = '<div class="help-block">';
$html[] = implode(LF, $allowedTablesHtml);
$html[] = '</div>';
$result['html'] = implode(LF, $html);
return $result;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,322 @@
<?php
namespace TYPO3\CMS\Backend\Form;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Create and return a defined array of data ready to be used by the
* container / element render part of FormEngine
*/
class FormDataCompiler
{
/**
* Data group that provides data
*
* @var FormDataGroupInterface
*/
protected $formDataGroup;
/**
* List of top level array elements to be unset from
* result array before final result is returned.
*
* @var array
*/
protected $removeKeysFromFinalResultArray = [
];
/**
* Get form data group injected
*
* @param FormDataGroupInterface $formDataGroup
*/
public function __construct(FormDataGroupInterface $formDataGroup)
{
$this->formDataGroup = $formDataGroup;
}
/**
* Main entry method maps given data input array and sanitizes some
* crucial input parameters and calls compile on FormDataGroupInterface.
*
* @param array $initialData Initial set of data to map into result array
* @return array Result with data
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
public function compile(array $initialData)
{
$result = $this->initializeResultArray();
// There must be only keys that actually exist in result data.
$keysNotInResult = array_diff(array_keys($initialData), array_keys($result));
if (!empty($keysNotInResult)) {
throw new \InvalidArgumentException(
'Array keys ' . implode(',', $keysNotInResult) . ' do not exist in result array and can not be set',
1440601540
);
}
foreach ($initialData as $dataKey => $dataValue) {
if ($dataKey === 'command') {
// Sanitize $command
if ($dataValue !== 'edit' && $dataValue !== 'new') {
throw new \InvalidArgumentException('Command must be either "edit" or "new"', 1437653136);
}
}
if ($dataKey === 'tableName') {
// Sanitize $tableName
if (empty($dataValue)) {
throw new \InvalidArgumentException('No $tableName given', 1437654409);
}
}
if ($dataKey === 'vanillaUid') {
if (!MathUtility::canBeInterpretedAsInteger($dataValue) && strpos($dataValue, 'NEW') !== 0) {
throw new \InvalidArgumentException('$vanillaUid is not an integer or "NEW..." string ID', 1437654247);
}
if (isset($initialData['command']) && $initialData['command'] === 'edit' && $dataValue < 0) {
throw new \InvalidArgumentException('Negative $vanillaUid is not supported with $command="edit', 1437654332);
}
}
$result[$dataKey] = $dataValue;
}
// Call the data group provider but take care it does not add or remove result keys
// This is basically a safety measure against data providers colliding with our array "contract"
$resultKeysBeforeFormDataGroup = array_keys($result);
$result = $this->formDataGroup->compile($result);
if (!is_array($result)) {
throw new \UnexpectedValueException(
'Data group provider must return array',
1446664764
);
}
if (!empty($result['renderData'])) {
throw new \RuntimeException(
'Array \'renderData\' not empty. Data providers must not add data here',
1485201279
);
}
$resultKeysAfterFormDataGroup = array_keys($result);
if ($resultKeysAfterFormDataGroup !== $resultKeysBeforeFormDataGroup) {
throw new \UnexpectedValueException(
'Data group provider must not change result key list',
1438079402
);
}
// Remove some data elements form result that are data provider internal and should
// not be exposed to calling object.
foreach ($this->removeKeysFromFinalResultArray as $key) {
unset($result[$key]);
}
return $result;
}
/**
* @return array
*/
protected function initializeResultArray()
{
return [
// Either "edit" or "new"
'command' => '',
// Table name of the handled row
'tableName' => '',
// Not changed uid of the record, meaning of value depends on context (new / edit)
// * If $command is "edit"
// ** $vanillaUid is a positive integer > 0 pointing to the record in the table
// * If $command is "new":
// ** If $vanillaUid > 0, it is the uid of a page the record should be added at
// ** If $vanillaUid < 0, it is the uid of a record in the same table after which the new record should be added
// ** If $vanillaUid = 0, a new record is added on page 0
// ** If $vanillaUid = "NEW..." Id of a parent page record if an inline child is added to a not yet persisted page
'vanillaUid' => 0,
// Url to return to
'returnUrl' => null,
// Title of the handled record.
'recordTitle' => '',
// Parent page record is either the full row of the parent page the record is located at or should
// be added to, or it is NULL, if a record is added or edited below the root page node.
'parentPageRow' => null,
// If a translated page is handled, the page row of the default language (the page against all page checks
// are made) is set here
'defaultLanguagePageRow' => null,
// Holds the "neighbor" row if incoming vanillaUid is negative and record creation is relative to a row of the same table.
'neighborRow' => null,
// For "new" this is the fully initialized row with defaults
// The database row. For "edit" fixVersioningPid() was applied already.
// @todo: rename to valueStructure or handledData or similar
'databaseRow' => [],
// The "effective" page uid we're working on. This is the uid of a page if a page is edited, or the uid
// of the parent page if a page or other record is added, or 0 if a record is added or edited below root node.
'effectivePid' => 0,
// Rootline of page the record that is handled is located at as created by BackendUtility::BEgetRootline()
'rootline' => [],
// The resolved SiteInterface object of the page or page given record is located on.
'site' => null,
// For "edit", this is the permission bitmask of the page that is edited, or of the page a record is located at
// For "new", this is the permission bitmask of the page the record is added to
// @todo: Remove if not needed on a lower level
'userPermissionOnPage' => 0,
// Full user TsConfig
'userTsConfig' => [],
// Full page TSConfig of the page that is edited or of the parent page if a record is added.
// This includes any defaultPageTSconfig and is merged with user TsConfig page. section. After type
// of handled record was determined, record type specific settings [TCEFORM.][tableName.][field.][types.][type.]
// are merged into [TCEFORM.][tableName.][field.]. Array keys still contain the concatenation dots.
'pageTsConfig' => [],
// Not changed TCA of parent page row if record is edited or added below a page and not root node
'vanillaParentPageTca' => null,
// List of available system languages. Array key is the system language uid, value array
// contains details of the record, with iso code resolved. Key is the sys_language_uid uid.
'systemLanguageRows' => [],
// If the page that is handled has "page_language_overlay" records (page has localizations in
// different languages), then this array holds those rows.
'pageLanguageOverlayRows' => [],
// If the handled row is a localized row, this entry hold the default language row array
'defaultLanguageRow' => null,
// If the handled row is a localived row and $TCA[<tableName>]['ctrl']['translationSource'] is configured,
// This entry holds the row of the language source record.
'sourceLanguageRow' => null,
// If the handled row is a localized row and a transOrigDiffSourceField is defined, this
// is the unserialized version of it. The diff source field is basically a shadow version
// of the default language record at the time when the language overlay record was created.
// This is used later to compare the default record with this content to show a "diff" if
// the default language record changed meanwhile.
'defaultLanguageDiffRow' => null,
// With userTS options.additionalPreviewLanguages set, field values of additional languages
// can be shown. This array holds those additional language records, Array key is sys_language_uid.
'additionalLanguageRows' => [],
// The tca record type value of the record. Forced to string, there can be "named" type values.
'recordTypeValue' => '',
// TCA of table with processed fields. After processing, this array contains merged and resolved
// array data, items were resolved, only used types are set, renderTypes are set.
'processedTca' => [],
// List of columns to be processed by data provider. Array value is the column name.
'columnsToProcess' => [],
// If set to TRUE, no wizards are calculated and rendered later
'disabledWizards' => false,
// Flex form field data handling is done in a separated FormDataCompiler instance. The full databaseRow
// of the record this flex form is embedded in is transferred in case features like single fields
// itemsProcFunc need to have this data at hand to do their job.
'flexParentDatabaseRow' => [],
// If not empty, it tells the TcaFlexProcess data provider to not calculate existing flex fields and
// existing flex container sections, but to instead prepare field values and the data structure TCA
// for a new container section. This is used by FormFlexAjaxController, the array contains details
// which container of which flex field should be created.
'flexSectionContainerPreparation' => [],
// If true, TcaSelectTreeItems data provider will compile tree items. This is false by default since
// on opening a record items are not calculated but are fetch in an ajax request, see FormSelectTreeAjaxController.
'selectTreeCompileItems' => false,
// BackendUser->uc['inlineView'] - This array holds status of expand / collapsed inline items
// This array is "flat", an inline structure with parent uid 1 having firstChild uid 2 having secondChild uid 3
// firstChild and secondChild are not nested. If an uid is set it means "record is expanded", example:
// 'parent' => [
// 1 => [
// 'firstChild' => [ 2 ], // record 2 of firstChild table is open in inline context to parent 1
// 'secondChild' => [ 3 ], // record 3 of secondChild table is open in inline context to parent 1
// ],
// ]
'inlineExpandCollapseStateArray' => [],
// The "entry" pid for inline records. Nested inline records can potentially hang around on different
// pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes
// place on the page structure. This is usually an int, but can be a "NEW..." string if an inline relation
// is added to a currently being created page.
'inlineFirstPid' => null,
// The "config" section of an inline parent, prepared and sanitized by TcaInlineConfiguration provider
'inlineParentConfig' => [],
// Flag that is enabled if a records is child of an inline parent
'isInlineChild' => false,
// Flag if an inline child is expanded so that additional fields need to be rendered
'isInlineChildExpanded' => false,
// Flag if the inline is in an ajax context that wants to expand the element
'isInlineAjaxOpeningContext' => false,
// Uid of the direct parent of the inline element. Handled as string since it may be a "NEW123" string
'inlineParentUid' => '',
// Table name of the direct parent of the inline element
'inlineParentTableName' => '',
// Field name of the direct parent of the inline element
'inlineParentFieldName' => '',
// Uid of the top most parent element. Handled as string since it may be a "NEW123" string
'inlineTopMostParentUid' => '',
// Table name of the top most parent element
'inlineTopMostParentTableName' => '',
// Field name of the top most parent element
'inlineTopMostParentFieldName' => '',
// If is on symetric side of an inline child parent reference.
// symmetric side can be achieved in case of an mm relation to the same table. If record A has a relation
// to record B, the symmetric side is set in case that record B gets edited.
// Record A (table1) <=> mm <=> Record B (table1)
'isOnSymmetricSide' => false,
// Uid of a "child-child" if a new record of an intermediate table is compiled to an existing child. This
// happens if foreign_selector in parent inline config is set. It will be used by default database row
// data providers to set this as value for the foreign_selector field on the intermediate table. One use
// case is FAL, where for instance a tt_content parent adds relation to an existing sys_file record and
// should set the uid of the existing sys_file record as uid_local - the foreign_selector of this inline
// configuration - of the new intermediate sys_file_reference record. Data provider that are called later
// will then use this relation to resolve for instance input placeholder relation values.
'inlineChildChildUid' => null,
// Inline scenario: A localized parent record is handled, so inline parents can have localized children.
// This value is set to TRUE if this array represents a default language
// child record that was not yet localized.
'isInlineDefaultLanguageRecordInLocalizedParentContext' => false,
// If set, inline children will be resolved. This is set to FALSE in inline ajax context where new children
// are created and existing children don't matter much.
'inlineResolveExistingChildren' => true,
// @todo - for input placeholder inline to suppress an infinite loop, this *may* become obsolete if
// @todo compilation of certain fields is possible
'inlineCompileExistingChildren' => true,
// @todo: keys below must be handled / further defined
'elementBaseName' => '',
'tabAndInlineStack' => [],
'inlineData' => [],
'inlineStructure' => [],
// This array of fields will be set as hidden-fields instead of rendered normally!
// This is used by EditDocumentController to force some field values if set as "overrideVals" in _GP
'overrideValues' => [],
// Default values for fields. This is for example used in DatabaseRowInitializeNew data provider to set
// fields to specific values if new records are created. Values are often carried around as "defVals" GET
// parameters and hand over by controllers to FormEngine using this array. Array structure is
// ['aTableName']['aFieldName'] = 'aValue'.
'defaultValues' => [],
// This array must NOT be set / manipulated by data providers but is instead used by the render part
// of FormEngine to add runtime data. Containers and elements add data here which is given to
// sub-containers, elements, controls and wizards.
'renderData' => [],
// A place for non-core, additional, custom data providers to add data. If a data provider needs to add
// additional data to the data array that doesn't fit elsewhere, it can place it here to use it in the
// render part again. Data in here should be namespaced in a way that it does not collide with other
// data providers adding further data here. Using the extension key as array key could be a good idea.
'customData' => [],
];
}
}
@@ -0,0 +1,41 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataGroup;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataGroupInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* A data provider group for flex form elements
*/
class FlexFormSegment implements FormDataGroupInterface
{
/**
* Compile form data
*
* @param array $result Initialized result array
* @return array Result filled with data
* @throws \UnexpectedValueException
*/
public function compile(array $result)
{
$orderedProviderList = GeneralUtility::makeInstance(OrderedProviderList::class);
$orderedProviderList->setProviderList(
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['flexFormSegment']
);
return $orderedProviderList->compile($result);
}
}
@@ -0,0 +1,74 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataGroup;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataGroupInterface;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Calls provider given by setter. This group is used to call a hard coded list of providers
* in some situations where extendability is not wanted. Use with care if at all ...
*/
class OnTheFly implements FormDataGroupInterface
{
/**
* @var array<FormDataProviderInterface>
*/
protected $providerList = [];
/**
* Compile form data
*
* @param array $result Initialized result array
* @return array Result filled with data
* @throws \UnexpectedValueException
*/
public function compile(array $result)
{
if (empty($this->providerList)) {
throw new \UnexpectedValueException(
'Data provider list is empty, call setProviderList first',
1441108674
);
}
foreach ($this->providerList as $providerClassName) {
/** @var FormDataProviderInterface $provider */
$provider = GeneralUtility::makeInstance($providerClassName);
if (!$provider instanceof FormDataProviderInterface) {
throw new \UnexpectedValueException(
'Data provider ' . $providerClassName . ' must implement FormDataProviderInterface',
1441108719
);
}
$result = $provider->addData($result);
}
return $result;
}
/**
* Set list of providers to be called
*
* @param array $list Given list of Provider class names
*/
public function setProviderList(array $list)
{
$this->providerList = $list;
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FormDataGroup;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataGroupInterface;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Service\DependencyOrderingService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Calls provider with dependencies specified given by setter
*
* This group is used to call a list of providers in order by specified
* dependencies before/depends.
*/
class OrderedProviderList implements FormDataGroupInterface
{
/**
* @var FormDataProviderInterface[]
*/
protected $providerList = [];
/**
* Compile form data
*
* @param array $result Initialized result array
* @return array Result filled with data
* @throws \UnexpectedValueException
*/
public function compile(array $result): array
{
$orderingService = GeneralUtility::makeInstance(DependencyOrderingService::class);
$orderedDataProvider = $orderingService->orderByDependencies($this->providerList, 'before', 'depends');
foreach ($orderedDataProvider as $providerClassName => $providerConfig) {
if (isset($providerConfig['disabled']) && $providerConfig['disabled'] === true) {
// Skip this data provider if disabled by configuration
continue;
}
/** @var FormDataProviderInterface $provider */
$provider = GeneralUtility::makeInstance($providerClassName);
if (!$provider instanceof FormDataProviderInterface) {
throw new \UnexpectedValueException(
'Data provider ' . $providerClassName . ' must implement FormDataProviderInterface',
1485299408
);
}
$result = $provider->addData($result);
}
return $result;
}
/**
* Set list of providers to be called
*
* The dependencies of a provider are specified as:
*
* FormDataProvider::class => [
* 'before' => [AnotherFormDataProvider::class]
* 'depends' => [YetAnotherFormDataProvider::class]
* ]
*
* @param array $list Given list of Provider class names
* @see \TYPO3\CMS\Core\Service\DependencyOrderingService
*/
public function setProviderList(array $list)
{
$this->providerList = $list;
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FormDataGroup;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataGroupInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* A data provider group for site and language configuration.
*
* This data group is for data fetched from sites yml files,
* it is fed by "fake TCA" since there are no real db records.
*
* It's similar to "fullDatabaseRecord", with some unused TCA types
* kicked out and some own data providers for record data and inline handling.
*/
class SiteConfigurationDataGroup implements FormDataGroupInterface
{
/**
* Compile form data
*
* @param array $result Initialized result array
* @return array Result filled with data
* @throws \UnexpectedValueException
*/
public function compile(array $result)
{
$orderedProviderList = GeneralUtility::makeInstance(OrderedProviderList::class);
$orderedProviderList->setProviderList(
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['siteConfiguration']
);
return $orderedProviderList->compile($result);
}
}
@@ -0,0 +1,41 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataGroup;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataGroupInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* A data provider group for casual database records
*/
class TcaDatabaseRecord implements FormDataGroupInterface
{
/**
* Compile form data
*
* @param array $result Initialized result array
* @return array Result filled with data
* @throws \UnexpectedValueException
*/
public function compile(array $result)
{
$orderedProviderList = GeneralUtility::makeInstance(OrderedProviderList::class);
$orderedProviderList->setProviderList(
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaDatabaseRecord']
);
return $orderedProviderList->compile($result);
}
}
@@ -0,0 +1,41 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataGroup;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataGroupInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* A data provider group for database records used for input placeholder values
*/
class TcaInputPlaceholderRecord implements FormDataGroupInterface
{
/**
* Compile form data
*
* @param array $result Initialized result array
* @return array Result filled with data
* @throws \UnexpectedValueException
*/
public function compile(array $result)
{
$orderedProviderList = GeneralUtility::makeInstance(OrderedProviderList::class);
$orderedProviderList->setProviderList(
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaInputPlaceholderRecord']
);
return $orderedProviderList->compile($result);
}
}
@@ -0,0 +1,42 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataGroup;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataGroupInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* A data provider group dedicated for the type='select', renderType='selectTree'
* calculate tree items FormSelectTreeAjaxController.
*/
class TcaSelectTreeAjaxFieldData implements FormDataGroupInterface
{
/**
* Compile TCA tree items
*
* @param array $result Initialized result array
* @return array Result filled with data
* @throws \UnexpectedValueException
*/
public function compile(array $result)
{
$orderedProviderList = GeneralUtility::makeInstance(OrderedProviderList::class);
$orderedProviderList->setProviderList(
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaSelectTreeAjaxFieldData']
);
return $orderedProviderList->compile($result);
}
}
@@ -0,0 +1,30 @@
<?php
namespace TYPO3\CMS\Backend\Form;
/*
* 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!
*/
/**
* Interface must be implemented by form data group classes.
* This interface is type hinted in FormDataCompiler and is a wrapper to group single data provider.
*/
interface FormDataGroupInterface
{
/**
* Compile form data
*
* @param array $result Initialized result array
* @return array Result filled with data
*/
public function compile(array $result);
}
@@ -0,0 +1,86 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Extended by other provider that fetch records from database
*/
abstract class AbstractDatabaseRecordProvider implements LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* Fetch a record from database. Deleted records will NOT be fetched.
* Method is similar to BackendUtility::getRecord, but is more picky
* about input and result.
*
* @param string $tableName The table name to fetch record from
* @param int $uid Uid of record to fetch
* @return array Fetched record row
* @throws DatabaseRecordException|\InvalidArgumentException|\UnexpectedValueException|\RuntimeException
*/
protected function getRecordFromDatabase($tableName, $uid)
{
if ($uid <= 0) {
throw new \InvalidArgumentException(
'$uid must be positive integer, ' . $uid . ' given',
1437656456
);
}
$row = $this->getDatabaseRow($tableName, $uid);
if (empty($row)) {
// Indicates a runtime error (eg. record was killed by other editor meanwhile) can be caught elsewhere
// and transformed to a message to the user or something
throw new DatabaseRecordException(
'Record with uid ' . $uid . ' from table ' . $tableName . ' not found',
1437656081,
null,
$tableName,
(int)$uid
);
}
return $row;
}
/**
* Retrieve the requested row from the database
*
* @param string $tableName
* @param int $uid
* @return array
*/
protected function getDatabaseRow(string $tableName, int $uid): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$row = $queryBuilder->select('*')
->from($tableName)
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
->execute()
->fetch();
return $row ?: [];
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,41 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Fetch page in default language from database if it's a translated pages record
*/
class DatabaseDefaultLanguagePageRow extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
/**
* Add default language page row of existing row to result
* defaultLanguagePageRow will stay NULL in result if a record is added or edited below root node
*
* @param array $result
* @return array
*/
public function addData(array $result)
{
// $defaultLanguagePageRow end up NULL if a record added or edited on root node
$tableName = $result['tableName'];
if ($tableName === 'pages' && $result['databaseRow'][$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0) {
$result['defaultLanguagePageRow'] = $this->getRecordFromDatabase('pages', $result['databaseRow'][$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']]);
}
return $result;
}
}
@@ -0,0 +1,52 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
/**
* Fetch existing database row on edit
*/
class DatabaseEditRow extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
/**
* Fetch existing record from database
*
* @param array $result
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
if ($result['command'] !== 'edit' || !empty($result['databaseRow'])) {
return $result;
}
$databaseRow = $this->getRecordFromDatabase($result['tableName'], $result['vanillaUid']);
if (!array_key_exists('pid', $databaseRow)) {
throw new \UnexpectedValueException(
'Parent record does not have a pid field',
1437663061
);
}
// Warning: By reference! In case the record is in a workspace, the -1 in pid will be
// changed to the real pid of the life record here.
BackendUtility::fixVersioningPid($result['tableName'], $databaseRow);
$result['databaseRow'] = $databaseRow;
return $result;
}
}
@@ -0,0 +1,45 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Set effective pid we're working on
*/
class DatabaseEffectivePid implements FormDataProviderInterface
{
/**
* Effective pid is used to determine entry point for page ts and is also
* the pid where new records are stored later.
*
* @param array $result
* @return array
*/
public function addData(array $result)
{
$effectivePid = 0;
if ($result['command'] === 'edit' && $result['tableName'] === 'pages') {
$effectivePid = $result['databaseRow']['uid'];
} elseif ($result['command'] === 'edit') {
$effectivePid = $result['databaseRow']['pid'];
} elseif ($result['command'] === 'new' && is_array($result['parentPageRow'])) {
$effectivePid = $result['parentPageRow']['uid'];
}
$result['effectivePid'] = (int)$effectivePid;
return $result;
}
}
@@ -0,0 +1,138 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
use TYPO3\CMS\Backend\Form\Exception\DatabaseDefaultLanguageException;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Add language related data to result array
*/
class DatabaseLanguageRows implements FormDataProviderInterface
{
/**
* Fetch default language if handled record is a localized one,
* unserialize transOrigDiffSourceField if it is defined,
* fetch additional languages if requested.
*
* @param array $result
* @return array
* @throws DatabaseDefaultLanguageException
*/
public function addData(array $result)
{
if (!empty($result['processedTca']['ctrl']['languageField'])
&& !empty($result['processedTca']['ctrl']['transOrigPointerField'])
) {
$languageField = $result['processedTca']['ctrl']['languageField'];
$fieldWithUidOfDefaultRecord = $result['processedTca']['ctrl']['transOrigPointerField'];
if (isset($result['databaseRow'][$languageField]) && $result['databaseRow'][$languageField] > 0
&& isset($result['databaseRow'][$fieldWithUidOfDefaultRecord]) && $result['databaseRow'][$fieldWithUidOfDefaultRecord] > 0
) {
// Default language record of localized record
$defaultLanguageRow = $this->getRecordWorkspaceOverlay(
$result['tableName'],
(int)$result['databaseRow'][$fieldWithUidOfDefaultRecord]
);
if (empty($defaultLanguageRow)) {
throw new DatabaseDefaultLanguageException(
'Default language record with id ' . (int)$result['databaseRow'][$fieldWithUidOfDefaultRecord]
. ' not found in table ' . $result['tableName'] . ' while editing record ' . $result['databaseRow']['uid'],
1438249426
);
}
$result['defaultLanguageRow'] = $defaultLanguageRow;
// Unserialize the "original diff source" if given
if (!empty($result['processedTca']['ctrl']['transOrigDiffSourceField'])
&& !empty($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']])
) {
$defaultLanguageKey = $result['tableName'] . ':' . (int)$result['databaseRow']['uid'];
$result['defaultLanguageDiffRow'][$defaultLanguageKey] = unserialize(
$result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']],
['allowed_classes' => false]
);
}
// Add language overlays from further localizations if requested
// @todo: Permission check if user is in "restrict ot language" is missing here.
// @todo: The TranslationConfigurationProvider is more stupid than good for us ... invent a better translation overlay api!
if (!empty($result['userTsConfig']['options.']['additionalPreviewLanguages'])) {
$additionalLanguageUids = GeneralUtility::intExplode(',', $result['userTsConfig']['options.']['additionalPreviewLanguages'], true);
/** @var TranslationConfigurationProvider $translationProvider */
$translationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
foreach ($additionalLanguageUids as $additionalLanguageUid) {
// Continue if this system language record does not exist or if 0 or -1 is requested
// or if row is the same as the to-be-displayed row
if ($additionalLanguageUid <= 0
|| !isset($result['systemLanguageRows'][$additionalLanguageUid])
|| $additionalLanguageUid === (int)$result['databaseRow'][$languageField]
) {
continue;
}
$translationInfo = $translationProvider->translationInfo(
$result['tableName'],
(int)$result['databaseRow'][$fieldWithUidOfDefaultRecord],
$additionalLanguageUid
);
if (!empty($translationInfo['translations'][$additionalLanguageUid]['uid'])) {
$record = $this->getRecordWorkspaceOverlay(
$result['tableName'],
(int)$translationInfo['translations'][$additionalLanguageUid]['uid']
);
$result['additionalLanguageRows'][$additionalLanguageUid] = $record;
}
}
}
// @todo do that only if l10n_parent > 0 (not in "free mode")?
if (!empty($result['processedTca']['ctrl']['translationSource'])
&& is_string($result['processedTca']['ctrl']['translationSource'])
) {
$translationSourceFieldName = $result['processedTca']['ctrl']['translationSource'];
if (isset($result['databaseRow'][$translationSourceFieldName])
&& $result['databaseRow'][$translationSourceFieldName] > 0
) {
$uidOfTranslationSource = $result['databaseRow'][$translationSourceFieldName];
$result['sourceLanguageRow'] = $this->getRecordWorkspaceOverlay(
$result['tableName'],
$uidOfTranslationSource
);
}
}
}
}
return $result;
}
/**
* Retrieve the requested row from the database
*
* @param string $tableName
* @param int $uid
* @return array
*/
protected function getRecordWorkspaceOverlay(string $tableName, int $uid): array
{
$row = BackendUtility::getRecordWSOL($tableName, $uid);
return $row ?: [];
}
}
@@ -0,0 +1,72 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fill the "pageLanguageOverlayRows" part of the result array
*/
class DatabasePageLanguageOverlayRows implements FormDataProviderInterface
{
/**
* Fetch available page overlay records of page
*
* @param array $result
* @return array
*/
public function addData(array $result)
{
if ($result['effectivePid'] === 0) {
// No overlays for records on pid 0 and not for new pages below root
return $result;
}
$result['pageLanguageOverlayRows'] = $this->getDatabaseRows((int)$result['effectivePid']);
return $result;
}
/**
* Retrieve the requested overlay row from the database
*
* @param int $pid
* @return array
*/
protected function getDatabaseRows(int $pid): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
$rows = $queryBuilder->select('*')
->from('pages')
->where($queryBuilder->expr()->eq(
$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
$queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
))
->execute()
->fetchAll();
return $rows;
}
}
@@ -0,0 +1,36 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
/**
* Set rootline
*/
class DatabasePageRootline implements FormDataProviderInterface
{
/**
* Fetch rootline
*
* @param array $result
* @return array
*/
public function addData(array $result)
{
$result['rootline'] = BackendUtility::BEgetRootLine($result['effectivePid'], '', true);
return $result;
}
}
@@ -0,0 +1,63 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Fetch parent page row from database if possible
*/
class DatabaseParentPageRow extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
/**
* Add parent page row of existing row to result
* parentPageRow will stay NULL in result if a record is added or edited below root node
*
* @param array $result
* @return array
*/
public function addData(array $result)
{
// $parentPageRow end up NULL if a record added or edited on root node
$parentPageRow = null;
if ($result['command'] === 'new') {
if ($result['vanillaUid'] < 0) {
// vanillaUid points to a neighbor record in same table - get its record and its pid from there to find parent record
$neighborRow = $this->getRecordFromDatabase($result['tableName'], abs($result['vanillaUid']));
if (!empty($neighborRow['t3ver_oid'])) {
$neighborRow = $this->getRecordFromDatabase($result['tableName'], (int)$neighborRow['t3ver_oid']);
}
$result['neighborRow'] = $neighborRow;
// uid of page the record is located in
$neighborRowPid = (int)$neighborRow['pid'];
if ($neighborRowPid !== 0) {
// Fetch the parent page record only if it is not the '0' root
$parentPageRow = $this->getRecordFromDatabase('pages', $neighborRowPid);
}
} elseif ($result['vanillaUid'] > 0) {
// vanillaUid points to a page uid directly
$parentPageRow = $this->getRecordFromDatabase('pages', $result['vanillaUid']);
}
} else {
// On "edit", the row itself has been fetched already
if ($result['databaseRow']['pid'] > 0) {
$parentPageRow = $this->getRecordFromDatabase('pages', $result['databaseRow']['pid']);
}
}
$result['parentPageRow'] = $parentPageRow;
return $result;
}
}
@@ -0,0 +1,45 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Determine the final TCA type value
*/
class DatabaseRecordOverrideValues implements FormDataProviderInterface
{
/**
* Add override values to the databaseRow fields. As those values are not meant to
* be overwritten by the user, the TCA of the field is set to type hidden.
*
* @param array $result
* @return array
*/
public function addData(array $result)
{
foreach ($result['overrideValues'] as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
$result['processedTca']['columns'][$fieldName]['config'] = [
'type' => 'hidden',
'renderType' => 'hidden',
];
}
}
return $result;
}
}
@@ -0,0 +1,154 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Determine the final TCA type value
*/
class DatabaseRecordTypeValue implements FormDataProviderInterface
{
/**
* TCA type value depends on several parameters. The simple case is
* a direct lookup in the database row, which then just needs handling
* in case the row is a localization overlay.
* More complex is the field:field syntax that can look up the actual
* value in a different table.
*
* @param array $result
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
if (!isset($result['processedTca']['types'])
|| !is_array($result['processedTca']['types'])
|| empty($result['processedTca']['types'])
) {
throw new \UnexpectedValueException(
'At least one "types" array must be defined for table ' . $result['tableName'] . ', preferred "0"',
1438185331
);
}
// Guard clause to suppress any calculation if record type value has been set from outside already
if ($result['recordTypeValue'] !== '') {
return $result;
}
$recordTypeValue = '0';
if (!empty($result['processedTca']['ctrl']['type'])) {
$tcaTypeField = $result['processedTca']['ctrl']['type'];
if (strpos($tcaTypeField, ':') === false) {
// $tcaTypeField is the name of a field in database row
if (!array_key_exists($tcaTypeField, $result['databaseRow'])) {
throw new \UnexpectedValueException(
'TCA table ' . $result['tableName'] . ' ctrl[\'type\'] is set to ' . $tcaTypeField . ', but'
. ' this field does not exist in the database of this table',
1438183881
);
}
$recordTypeValue = $result['databaseRow'][$tcaTypeField];
} else {
// If type is configured as localField:foreignField, fetch the type value from
// a foreign table. localField then point to a group or select field in the own table,
// this points to a record in a foreign table and the value of foreignField is then
// used as type field. This was introduced for some FAL scenarios.
list($pointerField, $foreignTableTypeField) = explode(':', $tcaTypeField);
$relationType = $result['processedTca']['columns'][$pointerField]['config']['type'];
if ($relationType !== 'select' && $relationType !== 'group') {
throw new \UnexpectedValueException(
'TCA foreign field pointer fields are only allowed to be used with group or select field types.'
. ' Handling field ' . $pointerField . ' with type configured as ' . $tcaTypeField,
1325862241
);
}
$foreignUid = $result['databaseRow'][$pointerField];
// Resolve the foreign record only if there is a uid, otherwise fall back 0
if (!empty($foreignUid)) {
// Determine table name to fetch record from
if ($relationType === 'select') {
$foreignTable = $result['processedTca']['columns'][$pointerField]['config']['foreign_table'] ?? '';
} else {
$allowedTables = explode(',', $result['processedTca']['columns'][$pointerField]['config']['allowed']);
// Always take the first configured table.
$foreignTable = $allowedTables[0];
}
if (empty($foreignTable)) {
throw new \UnexpectedValueException(
'No target table defined for type config field ' . $pointerField . ' of table ' . $result['tableName'],
1438253614
);
}
if (!MathUtility::canBeInterpretedAsInteger($foreignUid) && is_array($foreignUid[0])) {
// A group relation - has been resolved to array by TcaGroup data provider already
$foreignUid = $foreignUid[0]['uid'];
}
// Fetch field of this foreign row from db
if (MathUtility::canBeInterpretedAsInteger($foreignUid)) {
$foreignRow = $this->getDatabaseRow($foreignTable, (int)$foreignUid, $foreignTableTypeField);
if ($foreignRow[$foreignTableTypeField]) {
// @todo: It might be necessary to fetch the value from default language record as well here,
// @todo: this was buggy in the "old" implementation and never worked. It was therefor left out here for now.
// @todo: To implement that, see if the foreign row is a localized overlay, fetch default and merge exclude
$recordTypeValue = $foreignRow[$foreignTableTypeField];
}
}
}
}
}
// Throw another exception if determined value and '0' and '1' do not exist
if (empty($result['processedTca']['types'][$recordTypeValue])
&& empty($result['processedTca']['types']['0'])
&& empty($result['processedTca']['types']['1'])
) {
throw new \UnexpectedValueException(
'Type value ' . $recordTypeValue . ' from database record not defined in TCA of table '
. $result['tableName'] . ' and neither 0 nor 1 are defined as fallback.',
1438185437
);
}
// Check the determined value actually exists as types key, otherwise fall back to 0 or 1, 1 for "historical reasons"
if (empty($result['processedTca']['types'][$recordTypeValue])) {
$recordTypeValue = !empty($result['processedTca']['types']['0']) ? '0' : '1';
}
$result['recordTypeValue'] = (string)$recordTypeValue;
return $result;
}
/**
* Retrieve the requested row from the database
*
* @param string $tableName
* @param int $uid
* @param string $fieldName
* @return array
*/
protected function getDatabaseRow(string $tableName, int $uid, string $fieldName): array
{
$row = BackendUtility::getRecord($tableName, $uid, $fieldName);
return $row ?: [];
}
}
@@ -0,0 +1,56 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
/**
* Migrate date and datetime db field values to timestamp
*/
class DatabaseRowDateTimeFields implements FormDataProviderInterface
{
/**
* Migrate date and datetime db field values to timestamp
*
* @param array $result
* @return array
*/
public function addData(array $result)
{
$dateTimeTypes = QueryHelper::getDateTimeTypes();
$dateTimeFormats = QueryHelper::getDateTimeFormats();
foreach ($result['processedTca']['columns'] as $column => $columnConfig) {
if (isset($columnConfig['config']['dbType'])
&& in_array($columnConfig['config']['dbType'], $dateTimeTypes, true)
) {
if (!empty($result['databaseRow'][$column])
&& $result['databaseRow'][$column] !== $dateTimeFormats[$columnConfig['config']['dbType']]['empty']
) {
// Create an ISO-8601 date from current field data; the database always contains UTC
// The field value is something like "2016-01-01" or "2016-01-01 10:11:12", so appending "UTC"
// makes date() treat it as a UTC date (which is what we store in the database).
$result['databaseRow'][$column] = date('c', strtotime($result['databaseRow'][$column] . ' UTC'));
} else {
$result['databaseRow'][$column] = null;
}
}
// its a UNIX timestamp! We do not modify this here, as it will only be treated as a datetime because
// of eval being set to "date" or "datetime". This is handled in InputTextElement then.
}
return $result;
}
}
@@ -0,0 +1,69 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Handle TCA default values on row. This affects existing rows as well as new rows.
*
* Hint: Even after this class it is NOT safe no rely that *all* fields from
* columns are set in databaseRow.
*/
class DatabaseRowDefaultValues implements FormDataProviderInterface
{
/**
* Initialize new row with default values from various sources
*
* @param array $result
* @return array
*/
public function addData(array $result)
{
$databaseRow = $result['databaseRow'];
$newRow = $databaseRow;
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
// Keep current value if it can be resolved to "there is something" directly
if (isset($databaseRow[$fieldName])) {
$newRow[$fieldName] = $databaseRow[$fieldName];
continue;
}
// Special handling for eval null
if (!empty($fieldConfig['config']['eval']) && GeneralUtility::inList($fieldConfig['config']['eval'], 'null')) {
if (// Field exists and is set to NULL
array_key_exists($fieldName, $databaseRow)
// Default NULL is set, and this is a new record!
|| (array_key_exists('default', $fieldConfig['config']) && $fieldConfig['config']['default'] === null)
) {
$newRow[$fieldName] = null;
} else {
$newRow[$fieldName] = (string)$fieldConfig['config']['default'];
}
} else {
// Fun part: This forces empty string for any field even if no default is set. This is
// a useful side effect in flex form section containers where a new field is added to an existing
// value array because it was added to a data structure.
$newRow[$fieldName] = (string)$fieldConfig['config']['default'];
}
}
$result['databaseRow'] = $newRow;
return $result;
}
}
@@ -0,0 +1,301 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* On "new" command, initialize new database row with default data
*/
class DatabaseRowInitializeNew implements FormDataProviderInterface
{
/**
* Initialize new row with default values from various sources
* There are 4 sources of default values. Mind the order, the last takes precedence.
*
* @param array $result
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
if ($result['command'] !== 'new') {
return $result;
}
if (!is_array($result['databaseRow'])) {
throw new \UnexpectedValueException(
'databaseRow of table ' . $result['tableName'] . ' is not an array',
1444431128
);
}
$result = $this->setDefaultsFromUserTsConfig($result);
$result = $this->setDefaultsFromPageTsConfig($result);
$result = $this->setDefaultsFromNeighborRow($result);
$result = $this->setDefaultsFromDefaultValues($result);
$result = $this->setDefaultsFromInlineRelations($result);
$result = $this->setDefaultsFromInlineParentLanguage($result);
$result = $this->setDefaultsFromInlineParentUid($result);
$result = $this->setPid($result);
return $result;
}
/**
* Set defaults defined by user ts "TCAdefaults"
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromUserTsConfig(array $result)
{
$tableNameWithDot = $result['tableName'] . '.';
// Apply default values from user typo script "TCAdefaults" if any
if (isset($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
&& is_array($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
) {
foreach ($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot] as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
}
}
}
return $result;
}
/**
* Set defaults defined by page ts "TCAdefaults"
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromPageTsConfig(array $result)
{
$tableNameWithDot = $result['tableName'] . '.';
if (isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
&& is_array($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
) {
foreach ($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot] as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
}
}
}
return $result;
}
/**
* If a neighbor row is given (if vanillaUid was negative), field can be initialized with values
* from neighbor for fields registered in TCA['ctrl']['useColumnsForDefaultValues'].
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromNeighborRow(array $result)
{
if (is_array($result['neighborRow'])
&& !empty($result['processedTca']['ctrl']['useColumnsForDefaultValues'])
) {
$defaultColumns = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['useColumnsForDefaultValues'], true);
foreach ($defaultColumns as $fieldName) {
if (isset($result['processedTca']['columns'][$fieldName])
&& isset($result['neighborRow'][$fieldName])
) {
$result['databaseRow'][$fieldName] = $result['neighborRow'][$fieldName];
}
}
}
return $result;
}
/**
* Apply default values.
* These are typically carried around as "defVals" GET vars and set by controllers
* in $result['defaultValues'] array as init values.
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromDefaultValues(array $result)
{
$result = $this->setDefaultValuesFromGetPost($result);
$tableName = $result['tableName'];
$defaultValues = $result['defaultValues'] ?? [];
if (isset($defaultValues[$tableName]) && is_array($defaultValues[$tableName])) {
foreach ($defaultValues[$tableName] as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
}
}
}
return $result;
}
/**
* @param array $result
* @return array
* @deprecated since TYPO3 v9 will be removed in TYPO3 v10.0 - see $result['defaultValues']
*/
protected function setDefaultValuesFromGetPost(array $result)
{
if (!empty($result['defaultValues'])) {
return $result;
}
$defaultValues = GeneralUtility::_GP('defVals');
if (!empty($defaultValues)) {
trigger_error(
'Default values for new database rows should be set from controller context. Applying default values'
. ' via GET/POST parameters is deprecated since 9.2 and will be removed in version 10',
E_USER_DEPRECATED
);
$result['defaultValues'] = $defaultValues;
}
return $result;
}
/**
* Inline scenario if a new intermediate record to an existing child-child is
* compiled. Set "foreign_selector" field of this intermediate row to given
* "childChildUid". See TcaDataCompiler array comment of inlineChildChildUid
* for more details.
*
* @param array $result Result array
* @return array Modified result array
* @throws \UnexpectedValueException
*/
protected function setDefaultsFromInlineRelations(array $result)
{
if ($result['inlineChildChildUid'] === null) {
return $result;
}
if (!is_int($result['inlineChildChildUid'])) {
throw new \UnexpectedValueException(
'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but is not an integer',
1444434103
);
}
if (!isset($result['inlineParentConfig']['foreign_selector'])) {
throw new \UnexpectedValueException(
'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but no foreign_selector in inlineParentConfig',
1444434102
);
}
$selectorFieldName = $result['inlineParentConfig']['foreign_selector'];
if (!isset($result['processedTca']['columns'][$selectorFieldName]['config']['type'])
|| (
$result['processedTca']['columns'][$selectorFieldName]['config']['type'] !== 'select'
&& $result['processedTca']['columns'][$selectorFieldName]['config']['type'] !== 'group'
)
) {
throw new \UnexpectedValueException(
$selectorFieldName . ' is target type of a foreign_selector field to table ' . $result['tableName'] . ' and must be either a select or group type field',
1444434104
);
}
if ($result['inlineChildChildUid']) {
$result['databaseRow'][$selectorFieldName] = $result['inlineChildChildUid'];
}
return $result;
}
/**
* If a new child is created in an inline relation via ajax, and if the parent is a localized record,
* the child should have the same sys_language_uid set in the field declared in ['ctrl']['languageField']
* if the child is localizable itself.
* A localized parent transfers its sys_language_uid via inlineParentConfig['inline']['parentSysLanguageUid'],
* use that value as default for the child record languageField.
*
* @param array $result Result array
* @return array Modified result array
* @throws \UnexpectedValueException
*/
protected function setDefaultsFromInlineParentLanguage(array $result): array
{
if (!isset($result['inlineParentConfig']['inline']['parentSysLanguageUid'])
|| empty($result['processedTca']['ctrl']['languageField'])
|| empty($result['processedTca']['ctrl']['transOrigPointerField'])
) {
return $result;
}
if (!MathUtility::canBeInterpretedAsInteger($result['inlineParentConfig']['inline']['parentSysLanguageUid'])) {
throw new \UnexpectedValueException(
'A sys_language_uid is set from inline parent config but the value is no integer',
1490360772
);
}
$parentSysLanguageUid = (int)$result['inlineParentConfig']['inline']['parentSysLanguageUid'];
$languageFieldName = $result['processedTca']['ctrl']['languageField'];
$result['databaseRow'][$languageFieldName] = $parentSysLanguageUid;
return $result;
}
/**
* Set the parent uid of inline relations created via ajax to the corresponding foreign field
*
* @param array $result Result array
* @return array
*/
protected function setDefaultsFromInlineParentUid(array $result): array
{
$isInlineChild = $result['isInlineChild'] ?? false;
$parentField = $result['inlineParentConfig']['foreign_field'] ?? false;
if ($isInlineChild && $parentField && !empty($result['inlineParentUid'])) {
$result['databaseRow'][$parentField] = $result['inlineParentUid'];
}
return $result;
}
/**
* Set the pid. This is either the vanillaUid (see description in FormDataCompiler),
* or a pid given by pageTsConfig for inline children.
*
* @param array $result Result array
* @return array Modified result array
* @throws \UnexpectedValueException
*/
protected function setPid(array $result)
{
// Set pid to vanillaUid. This can be a negative value
// if the record is added relative to another record.
$result['databaseRow']['pid'] = $result['vanillaUid'];
// In case a new inline record is created, the pid can be set to a different value
// by pageTsConfig, but not by userTsConfig. This overrides the above pid selection
// and forces the pid of new inline children.
$tableNameWithDot = $result['tableName'] . '.';
if ($result['isInlineChild'] && isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'])) {
if (!MathUtility::canBeInterpretedAsInteger($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'])) {
throw new \UnexpectedValueException(
'page TSConfig setting TCAdefaults.' . $tableNameWithDot . 'pid must be a number, but given string '
. $result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'] . ' can not be interpreted as integer',
1461598332
);
}
$result['databaseRow']['pid'] = (int)$result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'];
}
return $result;
}
}
@@ -0,0 +1,110 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fill the "systemLanguageRows" part of the result array
*/
class DatabaseSystemLanguageRows implements FormDataProviderInterface
{
/**
* Fetch available system languages and resolve iso code if necessary.
*
* @param array $result
* @return array
* @throws \LogicException
*/
public function addData(array $result)
{
$site = $result['site'] ?? null;
if (!$site instanceof SiteInterface) {
throw new \LogicException(
'No valid site object found in $result[\'site\']',
1534952559
);
}
$pageIdDefaultLanguage = $result['defaultLanguagePageRow']['uid'] ?? $result['effectivePid'];
$languages = $site->getAvailableLanguages($this->getBackendUser(), true, $pageIdDefaultLanguage);
$languageRows = [];
foreach ($languages as $language) {
$languageId = $language->getLanguageId();
if ($languageId > 0) {
$iso = $language->getTwoLetterIsoCode();
} else {
$iso = 'DEF';
}
$languageRows[$languageId] = [
'uid' => $languageId,
'title' => $language->getTitle(),
'iso' => $iso,
'flagIconIdentifier' => $language->getFlagIdentifier()
];
if (empty($iso)) {
// No iso code could be found. This is currently possible in the system but discouraged.
// So, code within FormEngine has to be suited to work with an empty iso code. However,
// it may impact certain multi language scenarios, so we add a flash message hinting for
// incomplete configuration here.
// It might be possible to convert this to a non-catchable exception later if
// it iso code is enforced on a different layer of the system (tca required + migration wizard).
// @todo: This could be relaxed again if flex form language handling is extracted,
// @todo: since the rest of the FormEngine code does not rely on iso code?
$message = sprintf(
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.missingLanguageIsocode'),
$language->getTwoLetterIsoCode(),
$languageId
);
/** @var FlashMessage $flashMessage */
$flashMessage = GeneralUtility::makeInstance(
FlashMessage::class,
$message,
'',
FlashMessage::ERROR
);
/** @var FlashMessageService $flashMessageService */
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
}
}
$result['systemLanguageRows'] = $languageRows;
return $result;
}
/**
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,56 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* On "new" command, initialize uid with an unqique uid
*/
class DatabaseUniqueUidNewRow implements FormDataProviderInterface
{
/**
* Initialize new row with unique uid
*
* @param array $result
* @return array
* @throws \InvalidArgumentException
*/
public function addData(array $result)
{
if ($result['command'] !== 'new') {
return $result;
}
// Throw exception if uid is already set and does not start with NEW.
// In some situations a new record needs to be created again so the initialization of default
// values is triggered, but the "ID" of the new record is already known: This is the case if a
// new section container element is added by FormFlexAjaxController to a not yet persisted record.
// In this case, command "new" is given to the data compiler, but the "NEW1234" id has been calculated
// by the former compiler when opening the record already. The ajax controller then hands in the
// "new" command together with the id calculated by the first call.
if (isset($result['databaseRow']['uid']) && strpos($result['databaseRow']['uid'], 'NEW') !== 0) {
throw new \InvalidArgumentException(
'uid is already set to ' . $result['databaseRow']['uid'] . ' and does not start with NEW for a "new" command',
1437991120
);
}
if (!isset($result['databaseRow']['uid'])) {
$result['databaseRow']['uid'] = StringUtility::getUniqueId('NEW');
}
return $result;
}
}
@@ -0,0 +1,223 @@
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedContentEditException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedEditInternalsException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedHookException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedPageEditException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedPageNewException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedRootNodeException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedTableModifyException;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Determine user permission for action and check them
*/
class DatabaseUserPermissionCheck implements FormDataProviderInterface
{
/**
* Set userPermissionOnPage to result array and check access rights.
*
* A couple of different exceptions are thrown here:
* * If something weird happens a top level SPL exception is thrown.
* This indicates a non recoverable error.
* * If user has no access to whatever should be done, an exception that
* extends from Form\Exception\AccessDeniedException is thrown. This
* can be caught by upper level controller code and can be translated
* to a specific error message that is shown to the user depending on
* specific exception that is thrown.
*
* @param array $result
* @return array
* @throws AccessDeniedException|\LogicException|\RuntimeException
*/
public function addData(array $result)
{
$backendUser = $this->getBackendUser();
// Early return for admins
if ($backendUser->isAdmin()) {
$result['userPermissionOnPage'] = Permission::ALL;
return $result;
}
if (!$backendUser->check('tables_modify', $result['tableName'])) {
// If user has no modify rights on table, processing is stopped by throwing an
// exception immediately. This case can not be circumvented by hooks.
throw new AccessDeniedTableModifyException(
'No table modify permission for user ' . $backendUser->user['uid'] . ' on table ' . $result['tableName'],
1437683248
);
}
$exception = null;
$userHasAccess = false;
$userPermissionOnPage = Permission::NOTHING;
if ($result['command'] === 'new') {
// A new record is created. Access rights of parent record are important here
// @todo: In case of new inline child, parentPageRow should probably be the
// @todo: "inlineFirstPid" page - Maybe effectivePid and parentPageRow should be calculated differently then?
if (is_array($result['parentPageRow'])) {
// Record is added below an existing page
$userPermissionOnPage = $backendUser->calcPerms($result['parentPageRow']);
if ($result['tableName'] === 'pages') {
// New page is created, user needs PAGE_NEW for this
if ((bool)($userPermissionOnPage & Permission::PAGE_NEW)) {
$userHasAccess = true;
} else {
$exception = new AccessDeniedPageNewException(
'No page new permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['databaseRow']['uid'],
1437745640
);
}
} else {
// A regular record is added, not a page. User needs CONTENT_EDIT permission
if ((bool)($userPermissionOnPage & Permission::CONTENT_EDIT)) {
$userHasAccess = true;
} else {
$exception = new AccessDeniedContentEditException(
'No content new permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['parentPageRow']['uid'],
1437745759
);
}
}
} elseif (BackendUtility::isRootLevelRestrictionIgnored($result['tableName'])) {
// Non admin is creating a record on root node for a table that is actively allowed
$userHasAccess = true;
$userPermissionOnPage = Permission::ALL;
} else {
// Non admin has no create permission on root node records
$exception = new AccessDeniedRootNodeException(
'No record creation permission for user ' . $backendUser->user['uid'] . ' on page root node',
1437745221
);
}
} else {
// A page or a record on a page is edited
if ($result['tableName'] === 'pages') {
// A page record is edited, check edit rights of this record directly
$userPermissionOnPage = $backendUser->calcPerms($result['defaultLanguagePageRow'] ?? $result['databaseRow']);
if ((bool)($userPermissionOnPage & Permission::PAGE_EDIT) && $backendUser->check('pagetypes_select', $result['databaseRow'][$result['processedTca']['ctrl']['type']])) {
$userHasAccess = true;
} else {
$exception = new AccessDeniedPageEditException(
'No page edit permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['databaseRow']['uid'],
1437679336
);
}
} else {
// A non page record is edited.
if (isset($result['parentPageRow']) && is_array($result['parentPageRow'])) {
// If there is a parent page row, check content edit right of user
$userPermissionOnPage = $backendUser->calcPerms($result['parentPageRow']);
if ((bool)($userPermissionOnPage & Permission::CONTENT_EDIT)) {
$userHasAccess = true;
} else {
$exception = new AccessDeniedContentEditException(
'No content edit permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['parentPageRow']['uid'],
1437679657
);
}
} elseif (BackendUtility::isRootLevelRestrictionIgnored($result['tableName'])) {
// Non admin is editing a record on root node for a table that is actively allowed
$userHasAccess = true;
$userPermissionOnPage = Permission::ALL;
} else {
// Non admin has no edit permission on root node records
// @todo: This probably needs further handling, see http://review.typo3.org/40835
$exception = new AccessDeniedRootNodeException(
'No content edit permission for user ' . $backendUser->user['uid'] . ' on page root node',
1437679856
);
}
}
if ($userHasAccess) {
// If general access is allowed, check "recordEditAccessInternals"
$userHasAccess = $backendUser->recordEditAccessInternals($result['tableName'], $result['databaseRow']);
if (!$userHasAccess) {
$exception = new AccessDeniedEditInternalsException(
$backendUser->errorMsg,
1437687404
);
}
}
}
if ($userHasAccess && $exception) {
// Having user access TRUE here and an exception defined must not happen,
// indicates an internal error and throws a logic exception
throw new \LogicException(
'Access was TRUE but an exception was raised as well for table ' . $result['tableName'] . ' and user ' . $backendUser->user['uid'],
1437688402
);
}
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/alt_doc.php']['makeEditForm_accessCheck'] ?? null)) {
// A hook may modify the $userHasAccess decision. Previous state is saved to see if a hook changed
// a previous decision from TRUE to FALSE to throw a specific exception in this case
$userHasAccessBeforeHook = $userHasAccess;
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/alt_doc.php']['makeEditForm_accessCheck'] as $methodReference) {
$parameters = [
'table' => $result['tableName'],
'uid' => $result['databaseRow']['uid'],
'cmd' => $result['command'],
'hasAccess' => $userHasAccess,
];
$userHasAccess = (bool)GeneralUtility::callUserFunction($methodReference, $parameters, $this);
}
if ($userHasAccessBeforeHook && !$userHasAccess) {
$exception = new AccessDeniedHookException(
'Access to table ' . $result['tableName'] . ' for user ' . $backendUser->user['uid'] . ' was denied by a makeEditForm_accessCheck hook',
1437689705
);
}
if (!$userHasAccessBeforeHook && $userHasAccess) {
// Unset a previous exception if hook allowed access where previous checks didn't
$exception = null;
}
}
if (!$userHasAccess && !$exception) {
// User has no access, but no according exception was defined. This is an
// internal error and throws a logic exception.
throw new \LogicException(
'Access to table ' . $result['tableName'] . ' denied, but no reason given',
1437690507
);
}
if ($exception) {
throw $exception;
}
$result['userPermissionOnPage'] = $userPermissionOnPage;
return $result;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUser()
{
return $GLOBALS['BE_USER'];
}
}
Loaded 100 of 12716 files, more files were not shown because too many files have changed in this diff. Show more