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,91 @@
<?php
namespace TYPO3\CMS\Backend\Controller\Wizard;
/*
* 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\Template\DocumentTemplate;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
/**
* Class AbstractWizardController
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
class AbstractWizardController
{
/**
* Checks access for element
*
* @param string $table Table name
* @param int $uid Record uid
* @return bool
*/
protected function checkEditAccess($table, $uid)
{
$record = BackendUtility::getRecord($table, $uid);
BackendUtility::fixVersioningPid($table, $record);
if (is_array($record)) {
// If pages:
if ($table === 'pages') {
$calculatedPermissions = $this->getBackendUserAuthentication()->calcPerms($record);
$hasAccess = $calculatedPermissions & Permission::PAGE_EDIT;
} else {
// Fetching pid-record first.
$calculatedPermissions = $this->getBackendUserAuthentication()->calcPerms(
BackendUtility::getRecord('pages', $record['pid'])
);
$hasAccess = $calculatedPermissions & Permission::CONTENT_EDIT;
}
// Check internals regarding access:
if ($hasAccess) {
$hasAccess = $this->getBackendUserAuthentication()->recordEditAccessInternals($table, $record);
}
} else {
$hasAccess = false;
}
return (bool)$hasAccess;
}
/**
* Returns an instance of BackendUserAuthentication
*
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* Returns an instance of LanguageService
*
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
/**
* Returns an instance of DocumentTemplate
*
* @return DocumentTemplate
*/
protected function getDocumentTemplate()
{
return $GLOBALS['TBE_TEMPLATE'];
}
}
@@ -0,0 +1,342 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Controller\Wizard;
/*
* 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\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Script Class for adding new items to a group/select field. Performs proper redirection as needed.
* Script is typically called after new child record was added and then adds the new child to select value of parent.
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
class AddController extends AbstractWizardController
{
use PublicPropertyDeprecationTrait;
/**
* Properties which have been moved to protected status from public
*
* @var array
*/
protected $deprecatedPublicProperties = [
'content' => 'Using $content of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
'processDataFlag' => 'Using $processDataFlag of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
'pid' => 'Using $pid of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
'table' => 'Using $table of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
'id' => 'Using $id of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
'P' => 'Using $P of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
'returnEditConf' => 'Using $returnEditConf of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
];
/**
* Content accumulation for the module.
*
* @var string
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
*/
protected $content;
/**
* If set, the DataHandler class is loaded and used to add the returning ID to the parent record.
*
* @var int
*/
protected $processDataFlag = 0;
/**
* Create new record -pid (pos/neg). If blank, return immediately
*
* @var int
*/
protected $pid;
/**
* The parent table we are working on.
*
* @var string
*/
protected $table;
/**
* Loaded with the created id of a record FormEngine returns ...
*
* @var int
*/
protected $id;
/**
* Wizard parameters, coming from TCEforms linking to the wizard.
*
* @var array
*/
protected $P;
/**
* Information coming back from the FormEngine script, telling what the table/id was of the newly created record.
*
* @var string
*/
protected $returnEditConf;
/**
* Constructor
*/
public function __construct()
{
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_wizards.xlf');
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
$this->init($GLOBALS['TYPO3_REQUEST']);
}
/**
* Injects the request object for the current request or subrequest
* As this controller goes only through the main() method, it is rather simple for now
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function mainAction(ServerRequestInterface $request): ResponseInterface
{
return $this->processRequest($request);
}
/**
* Main function
* Will issue a location-header, redirecting either BACK or to a new FormEngine instance...
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function main()
{
trigger_error('AddController->main() will be replaced by protected method processRequest() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
$response = $this->processRequest($GLOBALS['TYPO3_REQUEST']);
HttpUtility::redirect($response->getHeaders()['location'][0]);
}
/**
* Initialization of the class.
* @param ServerRequestInterface $request
*/
protected function init(ServerRequestInterface $request): void
{
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
// Init GPvars:
$this->P = $parsedBody['P'] ?? $queryParams['P'] ?? [];
$this->returnEditConf = $parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? null;
// Get this record
$record = BackendUtility::getRecord($this->P['table'], $this->P['uid']);
// Set table:
$this->table = $this->P['params']['table'];
// Get TSconfig for it.
$TSconfig = BackendUtility::getTCEFORM_TSconfig(
$this->P['table'],
is_array($record) ? $record : ['pid' => $this->P['pid']]
);
// Set [params][pid]
if (strpos($this->P['params']['pid'], '###') === 0 && substr($this->P['params']['pid'], -3) === '###') {
$keyword = substr($this->P['params']['pid'], 3, -3);
if (strpos($keyword, 'PAGE_TSCONFIG_') === 0) {
$this->pid = (int)$TSconfig[$this->P['field']][$keyword];
} else {
$this->pid = (int)$TSconfig['_' . $keyword];
}
} else {
$this->pid = (int)$this->P['params']['pid'];
}
// Return if new record as parent (not possibly/allowed)
if ($this->pid === '') {
// HTTP Redirect is performed by processRequest()
return;
}
// Else proceed:
// If a new id has returned from a newly created record...
if ($this->returnEditConf) {
$editConfiguration = json_decode($this->returnEditConf, true);
if (is_array($editConfiguration[$this->table]) && MathUtility::canBeInterpretedAsInteger($this->P['uid'])) {
// Getting id and cmd from returning editConf array.
reset($editConfiguration[$this->table]);
$this->id = (int)key($editConfiguration[$this->table]);
$cmd = current($editConfiguration[$this->table]);
// ... and if everything seems OK we will register some classes for inclusion and instruct the object
// to perform processing later.
if ($this->P['params']['setValue']
&& $cmd === 'edit'
&& $this->id
&& $this->P['table']
&& $this->P['field'] && $this->P['uid']
) {
$liveRecord = BackendUtility::getLiveVersionOfRecord($this->table, $this->id, 'uid');
if ($liveRecord) {
$this->id = $liveRecord['uid'];
}
$this->processDataFlag = 1;
}
}
}
}
/**
* Main function
* Will issue a location-header, redirecting either BACK or to a new FormEngine instance...
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
protected function processRequest(ServerRequestInterface $request): ResponseInterface
{
// Return if new record as parent (not possibly/allowed)
if ($this->pid === '') {
return new RedirectResponse(GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']));
}
if ($this->returnEditConf) {
if ($this->processDataFlag) {
// Because OnTheFly can't handle MM relations with intermediate tables we use TcaDatabaseRecord here
// Otherwise already stored relations are overwritten with the new entry
/** @var TcaDatabaseRecord $formDataGroup */
$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
/** @var FormDataCompiler $formDataCompiler */
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
$input = [
'tableName' => $this->P['table'],
'vanillaUid' => (int)$this->P['uid'],
'command' => 'edit',
];
$result = $formDataCompiler->compile($input);
$currentParentRow = $result['databaseRow'];
// If that record was found (should absolutely be...), then init DataHandler and set, prepend or append
// the record
if (is_array($currentParentRow)) {
/** @var DataHandler $dataHandler */
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$data = [];
$recordId = $this->table . '_' . $this->id;
// Setting the new field data:
// If the field is a flexForm field, work with the XML structure instead:
if ($this->P['flexFormPath']) {
// Current value of flexForm path:
$currentFlexFormData = $currentParentRow[$this->P['field']];
/** @var FlexFormTools $flexFormTools */
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$currentFlexFormValueByPath = $flexFormTools->getArrayValueByPath(
$this->P['flexFormPath'],
$currentFlexFormData
);
// Compile currentFlexFormData to functional string
$currentFlexFormValues = [];
foreach ($currentFlexFormValueByPath as $value) {
if (is_array($value)) {
// group fields are always resolved to array
$currentFlexFormValues[] = $value['table'] . '_' . $value['uid'];
} else {
// but select fields may be uids only
$currentFlexFormValues[] = $value;
}
}
$currentFlexFormValue = implode(',', $currentFlexFormValues);
$insertValue = '';
switch ((string)$this->P['params']['setValue']) {
case 'set':
$insertValue = $recordId;
break;
case 'append':
$insertValue = $currentFlexFormValue . ',' . $recordId;
break;
case 'prepend':
$insertValue = $recordId . ',' . $currentFlexFormValue;
break;
}
$insertValue = implode(',', GeneralUtility::trimExplode(',', $insertValue, true));
$data[$this->P['table']][$this->P['uid']][$this->P['field']] = [];
$flexFormTools->setArrayValueByPath(
$this->P['flexFormPath'],
$data[$this->P['table']][$this->P['uid']][$this->P['field']],
$insertValue
);
} else {
$currentValue = $currentParentRow[$this->P['field']];
// Normalize CSV values
if (!is_array($currentValue)) {
$currentValue = GeneralUtility::trimExplode(',', $currentValue, true);
}
// Normalize all items to "<table>_<uid>" format
$currentValue = array_map(function ($item) {
// Handle per-item table for "group" elements
if (is_array($item)) {
$item = $item['table'] . '_' . $item['uid'];
} else {
$item = $this->table . '_' . $item;
}
return $item;
}, $currentValue);
switch ((string)$this->P['params']['setValue']) {
case 'set':
$currentValue = [$recordId];
break;
case 'append':
$currentValue[] = $recordId;
break;
case 'prepend':
array_unshift($currentValue, $recordId);
break;
}
$data[$this->P['table']][$this->P['uid']][$this->P['field']] = implode(',', $currentValue);
}
// Submit the data:
$dataHandler->start($data, []);
$dataHandler->process_datamap();
}
}
// Return to the parent FormEngine record editing session:
return new RedirectResponse(GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']));
}
// Redirecting to FormEngine with instructions to create a new record
// AND when closing to return back with information about that records ID etc.
/** @var \TYPO3\CMS\Core\Http\NormalizedParams */
$normalizedParams = $request->getAttribute('normalizedParams');
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
'returnEditConf' => 1,
'edit[' . $this->P['params']['table'] . '][' . $this->pid . ']' => 'new',
'returnUrl' => $normalizedParams->getRequestUri(),
]);
return new RedirectResponse($redirectUrl);
}
}
@@ -0,0 +1,225 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Controller\Wizard;
/*
* 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\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Script Class for redirecting a backend user to the editing form when an "Edit wizard" link was clicked in FormEngine somewhere
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
class EditController extends AbstractWizardController
{
use PublicPropertyDeprecationTrait;
/**
* Properties which have been moved to protected status from public
*
* @var array
*/
protected $deprecatedPublicProperties = [
'P' => 'Using $P of class EditController from the outside is discouraged, as this variable is only used for internal storage.',
'doClose' => 'Using $doClose of class EditController from the outside is discouraged, as this variable is only used for internal storage.',
];
/**
* Wizard parameters, coming from FormEngine linking to the wizard.
*
* Contains the following parts:
* - table
* - field
* - formName
* - hmac
* - fieldChangeFunc
* - fieldChangeFuncHash
* - currentValue
* - currentSelectedValues
*
* @var array
*/
protected $P;
/**
* Boolean; if set, the window will be closed by JavaScript
*
* @var int
*/
protected $doClose;
/**
* A little JavaScript to close the open window.
*
* @var string
*/
protected $closeWindow = '<script language="javascript" type="text/javascript">close();</script>';
/**
* Constructor
*/
public function __construct()
{
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_wizards.xlf');
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
$this->init($GLOBALS['TYPO3_REQUEST']);
}
/**
* Initialization of the script
*
* @param ServerRequestInterface $request
*/
protected function init(ServerRequestInterface $request)
{
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$this->P = $parsedBody['P'] ?? $queryParams['P'] ?? [];
// Used for the return URL to FormEngine so that we can close the window.
$this->doClose = $parsedBody['doClose'] ?? $queryParams['doClose'] ?? 0;
}
/**
* Injects the request object for the current request or subrequest
* As this controller goes only through the main() method, it is rather simple for now
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function mainAction(ServerRequestInterface $request): ResponseInterface
{
$content = $this->processRequest($request);
return $content;
}
/**
* Main function
* Makes a header-location redirect to an edit form IF POSSIBLE from the passed data - otherwise the window will
* just close.
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
* @return string
*/
public function main()
{
trigger_error('EditController->main() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
$request = $GLOBALS['TYPO3_REQUEST'];
$response = $this->processRequest($request);
if ($response instanceof RedirectResponse) {
HttpUtility::redirect($response->getHeaders()['location'][0]);
} else {
return $response->getBody()->getContents();
}
}
/**
* Process request function
* Makes a header-location redirect to an edit form IF POSSIBLE from the passed data - otherwise the window will
* just close.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
protected function processRequest(ServerRequestInterface $request): ResponseInterface
{
if ($this->doClose) {
return new HtmlResponse($this->closeWindow);
}
// Initialize:
$table = $this->P['table'];
$field = $this->P['field'];
if (empty($this->P['flexFormDataStructureIdentifier'])) {
// If there is not flex data structure identifier, field config is found in globals
$config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
} else {
// If there is a flex data structure identifier, parse that data structure and
// fetch config defined by given flex path
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$dataStructure = $flexFormTools->parseDataStructureByIdentifier($this->P['flexFormDataStructureIdentifier']);
$config = $flexFormTools->getArrayValueByPath($this->P['flexFormDataStructurePath'], $dataStructure);
if (!is_array($config)) {
throw new \RuntimeException(
'Something went wrong finding flex path ' . $this->P['flexFormDataStructurePath']
. ' in data structure identified by ' . $this->P['flexFormDataStructureIdentifier'],
1537356346
);
}
}
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
$urlParameters = [
'returnUrl' => (string)$uriBuilder->buildUriFromRoute('wizard_edit', ['doClose' => 1])
];
// Detecting the various allowed field type setups and acting accordingly.
if (is_array($config)
&& $config['type'] === 'select'
&& !$config['MM']
&& $config['maxitems'] <= 1
&& MathUtility::canBeInterpretedAsInteger($this->P['currentValue'])
&& $this->P['currentValue']
&& $config['foreign_table']
) {
// SINGLE value
$urlParameters['edit[' . $config['foreign_table'] . '][' . $this->P['currentValue'] . ']'] = 'edit';
// Redirect to FormEngine
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
return new RedirectResponse($url);
}
if (is_array($config)
&& $this->P['currentSelectedValues']
&& (
$config['type'] === 'select'
&& $config['foreign_table']
|| $config['type'] === 'group'
&& $config['internal_type'] === 'db'
)
) {
// MULTIPLE VALUES:
// Init settings:
$allowedTables = $config['type'] === 'group' ? $config['allowed'] : $config['foreign_table'];
$prependName = 1;
// Selecting selected values into an array:
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$relationHandler->start($this->P['currentSelectedValues'], $allowedTables);
$value = $relationHandler->getValueArray($prependName);
// Traverse that array and make parameters for FormEngine
foreach ($value as $rec) {
$recTableUidParts = GeneralUtility::revExplode('_', $rec, 2);
$urlParameters['edit[' . $recTableUidParts[0] . '][' . $recTableUidParts[1] . ']'] = 'edit';
}
// Redirect to FormEngine
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
return new RedirectResponse($url);
}
return new HtmlResponse($this->closeWindow);
}
}
@@ -0,0 +1,91 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Controller\Wizard;
/*
* 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\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* Wizard for rendering image manipulation view
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
class ImageManipulationController
{
/**
* @var StandaloneView
*/
private $templateView;
/**
* @param StandaloneView $templateView
*/
public function __construct(StandaloneView $templateView = null)
{
if (!$templateView) {
$templateView = GeneralUtility::makeInstance(StandaloneView::class);
$templateView->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts/')]);
$templateView->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials/ImageManipulation/')]);
$templateView->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ImageManipulation/ImageManipulationWizard.html'));
}
$this->templateView = $templateView;
}
/**
* Returns the HTML for the wizard inside the modal
*
* @param ServerRequestInterface $request
* @return ResponseInterface $response
*/
public function getWizardContent(ServerRequestInterface $request): ResponseInterface
{
if ($this->isSignatureValid($request)) {
$parsedBody = json_decode($request->getParsedBody()['arguments'], true);
$fileUid = $parsedBody['image'];
$image = null;
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
try {
$image = ResourceFactory::getInstance()->getFileObject($fileUid);
} catch (FileDoesNotExistException $e) {
}
}
$viewData = [
'image' => $image,
'cropVariants' => $parsedBody['cropVariants']
];
$content = $this->templateView->renderSection('Main', $viewData);
return new HtmlResponse($content);
}
return new HtmlResponse('', 403);
}
/**
* Check if hmac signature is correct
*
* @param ServerRequestInterface $request the request with the POST parameters
* @return bool
*/
protected function isSignatureValid(ServerRequestInterface $request): bool
{
$token = GeneralUtility::hmac($request->getParsedBody()['arguments'], 'ajax_wizard_image_manipulation');
return hash_equals($token, $request->getParsedBody()['signature']);
}
}
@@ -0,0 +1,157 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Controller\Wizard;
/*
* 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\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* Script Class for redirecting the user to the Web > List module if a wizard-link has been clicked in FormEngine
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
class ListController extends AbstractWizardController
{
use PublicPropertyDeprecationTrait;
/**
* Properties which have been moved to protected status from public
*
* @var array
*/
protected $deprecatedPublicProperties = [
'pid' => 'Using $pid of class ListController from the outside is discouraged, as this variable is only used for internal storage.',
'P' => 'Using $P of class ListController from the outside is discouraged, as this variable is only used for internal storage.',
'table' => 'Using $table of class ListController from the outside is discouraged, as this variable is only used for internal storage.',
'id' => 'Using $id of class ListController from the outside is discouraged, as this variable is only used for internal storage.',
];
/**
* @var int
*/
protected $pid;
/**
* Wizard parameters, coming from FormEngine linking to the wizard.
*
* @var array
*/
protected $P;
/**
* Table to show, if none, then all tables are listed in list module.
*
* @var string
*/
protected $table;
/**
* Page id to list.
*
* @var string
*/
protected $id;
/**
* Initialization of the class, setting GPvars.
*/
public function __construct()
{
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_wizards.xlf');
// @deprecated since TYPO3 v9, will be obsolete in TYPO3 v10.0
$request = $GLOBALS['TYPO3_REQUEST'];
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$this->P = $parsedBody['P'] ?? $queryParams['P'] ?? null;
$this->table = $parsedBody['table'] ?? $queryParams['table'] ?? null;
$this->id = $parsedBody['id'] ?? $queryParams['id'] ?? null;
}
/**
* Injects the request object for the current request or subrequest
* As this controller goes only through the main() method, it is rather simple for now
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function mainAction(ServerRequestInterface $request): ResponseInterface
{
$this->processRequest($request);
return new HtmlResponse('');
}
/**
* Main function
* Will issue a location-header, redirecting either BACK or to a new FormEngine instance...
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function main(): void
{
trigger_error('ListController->main() will be replaced by protected method processRequest() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
$this->processRequest($GLOBALS['TYPO3_REQUEST']);
}
/**
* Main function
* Will issue a location-header, redirecting either BACK or to a new FormEngine instance...
* @param ServerRequestInterface $request
*/
protected function processRequest(ServerRequestInterface $request): void
{
// Get this record
$origRow = BackendUtility::getRecord($this->P['table'], $this->P['uid']);
// Get TSconfig for it.
$TSconfig = BackendUtility::getTCEFORM_TSconfig(
$this->table,
is_array($origRow) ? $origRow : ['pid' => $this->P['pid']]
);
// Set [params][pid]
if (strpos($this->P['params']['pid'], '###') === 0 && substr($this->P['params']['pid'], -3) === '###') {
$keyword = substr($this->P['params']['pid'], 3, -3);
if (strpos($keyword, 'PAGE_TSCONFIG_') === 0) {
$this->pid = (int)$TSconfig[$this->P['field']][$keyword];
} else {
$this->pid = (int)$TSconfig['_' . $keyword];
}
} else {
$this->pid = (int)$this->P['params']['pid'];
}
// Make redirect:
// If pid is blank OR if id is set, then return...
if ((string)$this->id !== '') {
$redirectUrl = GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']);
} else {
/** @var UriBuilder $uriBuilder */
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$normalizedParams = $request->getAttribute('normalizedParams');
$requestUri = $normalizedParams->getRequestUri();
// Otherwise, show the list:
$urlParameters = [];
$urlParameters['id'] = $this->pid;
$urlParameters['table'] = $this->P['params']['table'];
$urlParameters['returnUrl'] = !empty($this->P['returnUrl'])
? GeneralUtility::sanitizeLocalUrl($this->P['returnUrl'])
: $requestUri;
$redirectUrl = (string)$uriBuilder->buildUriFromRoute('web_list', $urlParameters);
}
HttpUtility::redirect($redirectUrl);
}
}
@@ -0,0 +1,305 @@
<?php
namespace TYPO3\CMS\Backend\Controller\Wizard;
/*
* 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\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Receives ajax request from FormEngine suggest wizard and creates suggest answer as json result
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
class SuggestWizardController
{
/**
* Ajax handler for the "suggest" feature in FormEngine.
*
* @param ServerRequestInterface $request
* @throws \RuntimeException for incomplete or invalid arguments
* @return ResponseInterface
*/
public function searchAction(ServerRequestInterface $request): ResponseInterface
{
$parsedBody = $request->getParsedBody();
$search = $parsedBody['value'] ?? null;
$tableName = $parsedBody['tableName'] ?? null;
$fieldName = $parsedBody['fieldName'] ?? null;
$uid = $parsedBody['uid'] ?? null;
$pid = isset($parsedBody['pid']) ? (int)$parsedBody['pid'] : 0;
$dataStructureIdentifier = '' ?? null;
if (!empty($parsedBody['dataStructureIdentifier'])) {
$dataStructureIdentifier = $parsedBody['dataStructureIdentifier'];
}
$flexFormSheetName = $parsedBody['flexFormSheetName'] ?? null;
$flexFormFieldName = $parsedBody['flexFormFieldName'] ?? null;
$flexFormContainerName = $parsedBody['flexFormContainerName'] ?? null;
$flexFormContainerFieldName = $parsedBody['flexFormContainerFieldName'] ?? null;
// Determine TCA config of field
if (empty($dataStructureIdentifier)) {
// Normal columns field
$fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
$fieldNameInPageTsConfig = $fieldName;
} else {
// A flex flex form field
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
if (empty($flexFormContainerFieldName)) {
// @todo: See if a path in pageTsConfig like "TCEForm.tableName.theContainerFieldName =" is useful and works with other pageTs, too.
$fieldNameInPageTsConfig = $flexFormFieldName;
if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
['el'][$flexFormFieldName]['TCEforms']['config'])
) {
throw new \RuntimeException(
'Specified path ' . $flexFormFieldName . ' not found in flex form data structure',
1480609491
);
}
$fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
['el'][$flexFormFieldName]['TCEforms']['config'];
} else {
$fieldNameInPageTsConfig = $flexFormContainerFieldName;
if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
['el'][$flexFormFieldName]
['el'][$flexFormContainerName]
['el'][$flexFormContainerFieldName]['TCEforms']['config'])
) {
throw new \RuntimeException(
'Specified path ' . $flexFormContainerName . ' not found in flex form section container data structure',
1480611208
);
}
$fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
['el'][$flexFormFieldName]
['el'][$flexFormContainerName]
['el'][$flexFormContainerFieldName]['TCEforms']['config'];
}
}
$pageTsConfig = BackendUtility::getPagesTSconfig($pid);
$wizardConfig = $fieldConfig['suggestOptions'] ?? [];
$queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
$whereClause = $this->getWhereClause($fieldConfig);
$resultRows = [];
// fetch the records for each query table. A query table is a table from which records are allowed to
// be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
foreach ($queryTables as $queryTable) {
// if the table does not exist, skip it
if (!is_array($GLOBALS['TCA'][$queryTable]) || empty($GLOBALS['TCA'][$queryTable])) {
continue;
}
$config = $this->getConfigurationForTable($queryTable, $wizardConfig, $pageTsConfig, $tableName, $fieldNameInPageTsConfig);
// process addWhere
if (!isset($config['addWhere']) && $whereClause) {
$config['addWhere'] = $whereClause;
}
if (isset($config['addWhere'])) {
$replacement = [
'###THIS_UID###' => (int)$uid,
'###CURRENT_PID###' => (int)$pid
];
if (isset($pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'])) {
$fieldTSconfig = $pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'];
if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
$replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
}
if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
$replacement['###PAGE_TSCONFIG_IDLIST###'] = implode(',', GeneralUtility::intExplode(',', $fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
}
if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($fieldConfig['foreign_table']);
// nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping
// the whole field as it is not known where it is used in the WHERE clause
$replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\'');
}
}
$config['addWhere'] = strtr(' ' . $config['addWhere'], $replacement);
}
// instantiate the class that should fetch the records for this $queryTable
$receiverClassName = $config['receiverClass'];
if (!class_exists($receiverClassName)) {
$receiverClassName = SuggestWizardDefaultReceiver::class;
}
$receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
$params = [
'value' => $search,
'uid' => $uid,
];
$rows = $receiverObj->queryTable($params);
if (empty($rows)) {
continue;
}
$resultRows = $rows + $resultRows;
unset($rows);
}
// Limit the number of items in the result list
$maxItems = $config['maxItemsInResultList'] ?? 10;
$maxItems = min(count($resultRows), $maxItems);
array_splice($resultRows, $maxItems);
return (new JsonResponse())->setPayload(array_values($resultRows));
}
/**
* Returns TRUE if a table has been marked as hidden in the configuration
*
* @param array $tableConfig
* @return bool
*/
protected function isTableHidden(array $tableConfig)
{
return (bool)$tableConfig['ctrl']['hideTable'];
}
/**
* Checks if the current backend user is allowed to access the given table, based on the ctrl-section of the
* table's configuration array (TCA) entry.
*
* @param array $tableConfig
* @return bool
*/
protected function currentBackendUserMayAccessTable(array $tableConfig)
{
if ($this->getBackendUser()->isAdmin()) {
return true;
}
// If the user is no admin, they may not access admin-only tables
if ($tableConfig['ctrl']['adminOnly']) {
return false;
}
// allow access to root level pages if security restrictions should be bypassed
return !$tableConfig['ctrl']['rootLevel'] || $tableConfig['ctrl']['security']['ignoreRootLevelRestriction'];
}
/**
* Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the
* TSconfig.
*
* @param string $queryTable The table to query
* @param array $wizardConfig The configuration for the wizard as configured in the data structure
* @param array $TSconfig The TSconfig array of the current page
* @param string $table The table where the wizard is used
* @param string $field The field where the wizard is used
* @return array
*/
protected function getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
{
$config = (array)$wizardConfig['default'];
if (is_array($wizardConfig[$queryTable])) {
ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
}
$globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'];
$currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'];
// merge the configurations of different "levels" to get the working configuration for this table and
// field (i.e., go from the most general to the most special configuration)
if (is_array($globalSuggestTsConfig['default.'])) {
ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig['default.']);
}
if (is_array($globalSuggestTsConfig[$queryTable . '.'])) {
ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig[$queryTable . '.']);
}
// use $table instead of $queryTable here because we overlay a config
// for the input-field here, not for the queried table
if (is_array($currentFieldSuggestTsConfig['default.'])) {
ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig['default.']);
}
if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'])) {
ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig[$queryTable . '.']);
}
return $config;
}
/**
* Checks the given field configuration for the tables that should be used for querying and returns them as an
* array.
*
* @param array $fieldConfig
* @return array
*/
protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig)
{
$queryTables = [];
if (isset($fieldConfig['allowed'])) {
if ($fieldConfig['allowed'] !== '*') {
// list of allowed tables
$queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
} else {
// all tables are allowed, if the user can access them
foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
if (!$this->isTableHidden($tableConfig) && $this->currentBackendUserMayAccessTable($tableConfig)) {
$queryTables[] = $tableName;
}
}
unset($tableName, $tableConfig);
}
} elseif (isset($fieldConfig['foreign_table'])) {
// use the foreign table
$queryTables = [$fieldConfig['foreign_table']];
}
return $queryTables;
}
/**
* Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table
* is configured and should be used; it could e.g. be used to limit to a certain subset of records from the
* foreign table
*
* @param array $fieldConfig
* @return string
*/
protected function getWhereClause(array $fieldConfig)
{
if (!isset($fieldConfig['foreign_table'])) {
return '';
}
// strip ORDER BY clause
return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
}
/**
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
*/
protected function getBackendUser()
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,792 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Controller\Wizard;
/*
* 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\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Script Class for rendering the Table Wizard
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
class TableController extends AbstractWizardController
{
use PublicPropertyDeprecationTrait;
/**
* Properties which have been moved to protected status from public
*
* @var array
*/
protected $deprecatedPublicProperties = [
'content' => 'Using $content of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
'inputStyle' => 'Using $inputStyle of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
'xmlStorage' => 'Using $xmlStorage of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
'numNewRows' => 'Using $numNewRows of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
'colsFieldName' => 'Using $colsFieldName of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
'P' => 'Using $P of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
'TABLECFG' => 'Using $TABLECFG of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
'tableParsing_quote' => 'Using $tableParsing_quote of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
'tableParsing_delimiter' => 'Using $tableParsing_delimiter of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
];
/**
* Content accumulation for the module.
*
* @var string
*/
protected $content;
/**
* If TRUE, <input> fields are shown instead of textareas.
*
* @var bool
*/
protected $inputStyle = false;
/**
* If set, the string version of the content is interpreted/written as XML
* instead of the original line-based kind. This variable still needs binding
* to the wizard parameters - but support is ready!
*
* @var int
*/
protected $xmlStorage = 0;
/**
* Number of new rows to add in bottom of wizard
*
* @var int
*/
protected $numNewRows = 1;
/**
* Name of field in parent record which MAY contain the number of columns for the table
* here hardcoded to the value of tt_content. Should be set by FormEngine parameters (from P)
*
* @var string
*/
protected $colsFieldName = 'cols';
/**
* Wizard parameters, coming from FormEngine linking to the wizard.
*
* @var array
*/
protected $P;
/**
* The array which is constantly submitted by the multidimensional form of this wizard.
*
* @var array
*/
protected $TABLECFG;
/**
* Table parsing
* quoting of table cells
*
* @var string
*/
protected $tableParsing_quote;
/**
* delimiter between table cells
*
* @var string
*/
protected $tableParsing_delimiter;
/**
* @var IconFactory
*/
protected $iconFactory;
/**
* ModuleTemplate object
*
* @var ModuleTemplate
*/
protected $moduleTemplate;
/**
* Constructor
*/
public function __construct()
{
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_wizards.xlf');
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
$this->init($GLOBALS['TYPO3_REQUEST']);
}
/**
* Injects the request object for the current request or subrequest
* As this controller goes only through the main() method, it is rather simple for now
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function mainAction(ServerRequestInterface $request): ResponseInterface
{
$response = $this->renderContent($request);
if (empty($response)) {
$response = new HtmlResponse($this->moduleTemplate->renderContent());
}
return $response;
}
/**
* Main function, rendering the table wizard
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function main()
{
trigger_error('TableController->main() will be replaced by protected method renderContent() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
$response = $this->renderContent($GLOBALS['TYPO3_REQUEST']);
if ($response instanceof RedirectResponse) {
HttpUtility::redirect($response->getHeaders()['location'][0]);
}
}
/**
* Draws the table wizard content
*
* @return string HTML content for the form.
* @throws \RuntimeException
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function tableWizard()
{
trigger_error('TableController->tableWizard() will be replaced by protected method renderTableWizard() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
$result = $this->renderTableWizard($GLOBALS['TYPO3_REQUEST']);
if ($result instanceof RedirectResponse) {
HttpUtility::redirect($result->getHeaders()['location'][0]);
}
return $result;
}
/**
* Will get and return the configuration code string
* Will also save (and possibly redirect/exit) the content if a save button has been pressed
*
* @param array $row Current parent record row
* @return array Table config code in an array
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function getConfigCode($row)
{
trigger_error('TableController->getConfigCode() will be replaced by protected method getConfiguration() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
$result = $this->getConfiguration($row, $GLOBALS['TYPO3_REQUEST']);
if ($result instanceof RedirectResponse) {
HttpUtility::redirect($result->getHeaders()['location'][0]);
}
return $result;
}
/**
* Creates the HTML for the Table Wizard:
*
* @param array $configuration Table config array
* @return string HTML for the table wizard
* @internal
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function getTableHTML($configuration)
{
trigger_error('TableController->getTableHTML() will be replaced by protected method getTableWizard() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
return $this->getTableWizard($configuration);
}
/**
* Detects if a control button (up/down/around/delete) has been pressed for an item and accordingly it will
* manipulate the internal TABLECFG array
*
* @internal
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function changeFunc()
{
trigger_error('TableController->changeFunc() will be replaced by protected method manipulateTable() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
$this->manipulateTable();
}
/**
* Converts the input array to a configuration code string
*
* @param array $cfgArr Array of table configuration (follows the input structure from the table wizard POST form)
* @return string The array converted into a string with line-based configuration.
* @see cfgString2CfgArray()
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function cfgArray2CfgString($cfgArr)
{
trigger_error('TableController->cfgArray2CfgString() will be replaced by protected method configurationArrayToString() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
return $this->configurationArrayToString($cfgArr);
}
/**
* Converts the input configuration code string into an array
*
* @param string $configurationCode Configuration code
* @param int $columns Default number of columns
* @return array Configuration array
* @see cfgArray2CfgString()
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function cfgString2CfgArray($configurationCode, $columns)
{
trigger_error('TableController->cfgString2CfgArray() will be replaced by protected method configurationStringToArray() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
return $this->configurationStringToArray($configurationCode, $columns);
}
/**
* Initialization of the class
*
* @param ServerRequestInterface $request
*/
protected function init(ServerRequestInterface $request): void
{
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
// GPvars:
$this->P = $parsedBody['P'] ?? $queryParams['P'] ?? null;
$this->TABLECFG = $parsedBody['TABLE'] ?? $queryParams['TABLE'] ?? null;
// Setting options:
$this->xmlStorage = $this->P['params']['xmlOutput'];
$this->numNewRows = MathUtility::forceIntegerInRange($this->P['params']['numNewRows'], 1, 50, 5);
// Textareas or input fields:
$this->inputStyle = isset($this->TABLECFG['textFields']) ? (bool)$this->TABLECFG['textFields'] : true;
$this->tableParsing_delimiter = '|';
$this->tableParsing_quote = '';
}
/**
* Main function, rendering the table wizard
*
* @param ServerRequestInterface $request
* @return ResponseInterface|null
*/
protected function renderContent(ServerRequestInterface $request): ?ResponseInterface
{
$normalizedParams = $request->getAttribute('normalizedParams');
$requestUri = $normalizedParams->getRequestUri();
list($rUri) = explode('#', $requestUri);
$this->content .= '<form action="' . htmlspecialchars($rUri) . '" method="post" id="TableController" name="wizardForm">';
if ($this->P['table'] && $this->P['field'] && $this->P['uid']) {
$tableWizard = $this->renderTableWizard($request);
if ($tableWizard instanceof RedirectResponse) {
return $tableWizard;
}
$this->content .= '<h2>' . htmlspecialchars($this->getLanguageService()->getLL('table_title')) . '</h2>'
. '<div>' . $tableWizard . '</div>';
} else {
$this->content .= '<h2>' . htmlspecialchars($this->getLanguageService()->getLL('table_title')) . '</h2>'
. '<div><span class="text-danger">' . htmlspecialchars($this->getLanguageService()->getLL('table_noData')) . '</span></div>';
}
$this->content .= '</form>';
// Setting up the buttons and markers for docHeader
$this->getButtons();
// Build the <body> for the module
$this->moduleTemplate->setContent($this->content);
return null;
}
/**
* Create the panel of buttons for submitting the form or otherwise perform operations.
*/
protected function getButtons(): void
{
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
if ($this->P['table'] && $this->P['field'] && $this->P['uid']) {
// CSH
$cshButton = $buttonBar->makeHelpButton()
->setModuleName('xMOD_csh_corebe')
->setFieldName('wizard_table_wiz');
$buttonBar->addButton($cshButton);
// Close
$closeButton = $buttonBar->makeLinkButton()
->setHref($this->P['returnUrl'])
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL))
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
->setShowLabelText(true);
$buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
// Save
$saveButton = $buttonBar->makeInputButton()
->setName('_savedok')
->setValue('1')
->setForm('TableController')
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
->setShowLabelText(true);
$buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
// Reload
$reloadButton = $buttonBar->makeInputButton()
->setName('_refresh')
->setValue('1')
->setForm('TableController')
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL))
->setTitle($this->getLanguageService()->getLL('forms_refresh'));
$buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
}
}
/**
* Draws the table wizard content
*
* @param ServerRequestInterface $request
* @return string|ResponseInterface HTML content for the form.
* @throws \RuntimeException
*/
protected function renderTableWizard(ServerRequestInterface $request)
{
if (!$this->checkEditAccess($this->P['table'], $this->P['uid'])) {
throw new \RuntimeException('Wizard Error: No access', 1349692692);
}
// First, check the references by selecting the record:
$row = BackendUtility::getRecord($this->P['table'], $this->P['uid']);
if (!is_array($row)) {
throw new \RuntimeException('Wizard Error: No reference to record', 1294587125);
}
// This will get the content of the form configuration code field to us - possibly cleaned up,
// saved to database etc. if the form has been submitted in the meantime.
$tableCfgArray = $this->getConfiguration($row, $request);
if ($tableCfgArray instanceof ResponseInterface) {
return $tableCfgArray;
}
// Generation of the Table Wizards HTML code:
$content = $this->getTableWizard($tableCfgArray);
// Return content:
return $content;
}
/**
* Will get and return the configuration code string
* Will also save (and possibly redirect/exit) the content if a save button has been pressed
*
* @param array $row Current parent record row
* @param ServerRequestInterface $request
* @return array|ResponseInterface Table config code in an array
*/
protected function getConfiguration(array $row, ServerRequestInterface $request)
{
// Get delimiter settings
$this->tableParsing_quote = $row['table_enclosure'] ? chr((int)$row['table_enclosure']) : '';
$this->tableParsing_delimiter = $row['table_delimiter'] ? chr((int)$row['table_delimiter']) : '|';
// If some data has been submitted, then construct
if (isset($this->TABLECFG['c'])) {
// Process incoming:
$this->manipulateTable();
// Convert to string (either line based or XML):
if ($this->xmlStorage) {
// Convert the input array to XML:
$bodyText = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . GeneralUtility::array2xml($this->TABLECFG['c'], '', 0, 'T3TableWizard');
// Setting cfgArr directly from the input:
$configuration = $this->TABLECFG['c'];
} else {
// Convert the input array to a string of configuration code:
$bodyText = $this->configurationArrayToString($this->TABLECFG['c']);
// Create cfgArr from the string based configuration - that way it is cleaned up
// and any incompatibilities will be removed!
$configuration = $this->configurationStringToArray($bodyText, (int)$row[$this->colsFieldName]);
}
// If a save button has been pressed, then save the new field content:
if ($_POST['_savedok'] || $_POST['_saveandclosedok']) {
// Get DataHandler object:
/** @var DataHandler $dataHandler */
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
// Put content into the data array:
$data = [];
if ($this->P['flexFormPath']) {
// Current value of flexForm path:
$currentFlexFormData = GeneralUtility::xml2array($row[$this->P['field']]);
/** @var FlexFormTools $flexFormTools */
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$flexFormTools->setArrayValueByPath($this->P['flexFormPath'], $currentFlexFormData, $bodyText);
$data[$this->P['table']][$this->P['uid']][$this->P['field']] = $currentFlexFormData;
} else {
$data[$this->P['table']][$this->P['uid']][$this->P['field']] = $bodyText;
}
// Perform the update:
$dataHandler->start($data, []);
$dataHandler->process_datamap();
// If the save/close button was pressed, then redirect the screen:
if ($_POST['_saveandclosedok']) {
return new RedirectResponse(GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']));
}
}
} else {
// If nothing has been submitted, load the $bodyText variable from the selected database row:
if ($this->xmlStorage) {
$configuration = GeneralUtility::xml2array($row[$this->P['field']]);
} else {
if ($this->P['flexFormPath']) {
// Current value of flexForm path:
$currentFlexFormData = GeneralUtility::xml2array($row[$this->P['field']]);
/** @var FlexFormTools $flexFormTools */
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$configuration = $flexFormTools->getArrayValueByPath(
$this->P['flexFormPath'],
$currentFlexFormData
);
$configuration = $this->configurationStringToArray($configuration, 0);
} else {
// Regular line based table configuration:
$columns = $row[$this->colsFieldName] ?? 0;
$configuration = $this->configurationStringToArray($row[$this->P['field']] ?? '', (int)$columns);
}
}
$configuration = is_array($configuration) ? $configuration : [];
}
return $configuration;
}
/**
* Creates the HTML for the Table Wizard:
*
* @param array $configuration Table config array
* @return string HTML for the table wizard
*/
protected function getTableWizard(array $configuration): string
{
// Traverse the rows:
$tRows = [];
$k = 0;
$countLines = count($configuration);
foreach ($configuration as $cellArr) {
if (is_array($cellArr)) {
// Initialize:
$cells = [];
$a = 0;
// Traverse the columns:
foreach ($cellArr as $cellContent) {
if ($this->inputStyle) {
$cells[] = '<input class="form-control" type="text" name="TABLE[c][' . ($k + 1) * 2 . '][' . ($a + 1) * 2 . ']" value="' . htmlspecialchars($cellContent) . '" />';
} else {
$cellContent = preg_replace('/<br[ ]?[\\/]?>/i', LF, $cellContent);
$cells[] = '<textarea class="form-control" rows="6" name="TABLE[c][' . ($k + 1) * 2 . '][' . ($a + 1) * 2 . ']">' . htmlspecialchars($cellContent) . '</textarea>';
}
// Increment counter:
$a++;
}
// CTRL panel for a table row (move up/down/around):
$onClick = 'document.wizardForm.action+=' . GeneralUtility::quoteJSvalue('#ANC_' . (($k + 1) * 2 - 2)) . ';';
$onClick = ' onclick="' . htmlspecialchars($onClick) . '"';
$ctrl = '';
if ($k !== 0) {
$ctrl .= '<button class="btn btn-default" name="TABLE[row_up][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_up')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-up"></span></button>';
} else {
$ctrl .= '<button class="btn btn-default" name="TABLE[row_bottom][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_bottom')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-double-down"></span></button>';
}
if ($k + 1 !== $countLines) {
$ctrl .= '<button class="btn btn-default" name="TABLE[row_down][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_down')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-down"></span></button>';
} else {
$ctrl .= '<button class="btn btn-default" name="TABLE[row_top][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_top')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-double-up"></span></button>';
}
$ctrl .= '<button class="btn btn-default" name="TABLE[row_remove][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_removeRow')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-trash"></span></button>';
$ctrl .= '<button class="btn btn-default" name="TABLE[row_add][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_addRow')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-plus"></span></button>';
$tRows[] = '
<tr>
<td>
<a name="ANC_' . ($k + 1) * 2 . '"></a>
<span class="btn-group' . ($this->inputStyle ? '' : '-vertical') . '">' . $ctrl . '</span>
</td>
<td>' . implode('</td>
<td>', $cells) . '</td>
</tr>';
// Increment counter:
$k++;
}
}
// CTRL panel for a table column (move left/right/around/delete)
$cells = [];
$cells[] = '';
// Finding first row:
$firstRow = reset($configuration);
if (is_array($firstRow)) {
$cols = count($firstRow);
for ($a = 1; $a <= $cols; $a++) {
$b = $a * 2;
$ctrl = '';
if ($a !== 1) {
$ctrl .= '<button class="btn btn-default" name="TABLE[col_left][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_left')) . '"><span class="t3-icon fa fa-fw fa-angle-left"></span></button>';
} else {
$ctrl .= '<button class="btn btn-default" name="TABLE[col_end][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_end')) . '"><span class="t3-icon fa fa-fw fa-angle-double-right"></span></button>';
}
if ($a != $cols) {
$ctrl .= '<button class="btn btn-default" name="TABLE[col_right][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_right')) . '"><span class="t3-icon fa fa-fw fa-angle-right"></span></button>';
} else {
$ctrl .= '<button class="btn btn-default" name="TABLE[col_start][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_start')) . '"><span class="t3-icon fa fa-fw fa-angle-double-left"></span></button>';
}
$ctrl .= '<button class="btn btn-default" name="TABLE[col_remove][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_removeColumn')) . '"><span class="t3-icon fa fa-fw fa-trash"></span></button>';
$ctrl .= '<button class="btn btn-default" name="TABLE[col_add][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_addColumn')) . '"><span class="t3-icon fa fa-fw fa-plus"></span></button>';
$cells[] = '<span class="btn-group">' . $ctrl . '</span>';
}
$tRows[] = '
<tfoot>
<tr>
<td>' . implode('</td>
<td>', $cells) . '</td>
</tr>
</tfoot>';
}
$content = '';
$addSubmitOnClick = 'onclick="document.getElementById(\'TableController\').submit();"';
// Implode all table rows into a string, wrapped in table tags.
$content .= '
<!-- Table wizard -->
<div class="table-fit table-fit-inline-block">
<table id="typo3-tablewizard" class="table table-center">
' . implode('', $tRows) . '
</table>
</div>';
// Input type checkbox:
$content .= '
<!-- Input mode check box: -->
<div class="checkbox">
<input type="hidden" name="TABLE[textFields]" value="0" />
<label for="textFields">
<input type="checkbox" ' . $addSubmitOnClick . ' name="TABLE[textFields]" id="textFields" value="1"' . ($this->inputStyle ? ' checked="checked"' : '') . ' />
' . $this->getLanguageService()->getLL('table_smallFields') . '
</label>
</div>';
return $content;
}
/**
* Detects if a control button (up/down/around/delete) has been pressed for an item and accordingly it will
* manipulate the internal TABLECFG array
*/
protected function manipulateTable(): void
{
if ($this->TABLECFG['col_remove']) {
$kk = key($this->TABLECFG['col_remove']);
$cmd = 'col_remove';
} elseif ($this->TABLECFG['col_add']) {
$kk = key($this->TABLECFG['col_add']);
$cmd = 'col_add';
} elseif ($this->TABLECFG['col_start']) {
$kk = key($this->TABLECFG['col_start']);
$cmd = 'col_start';
} elseif ($this->TABLECFG['col_end']) {
$kk = key($this->TABLECFG['col_end']);
$cmd = 'col_end';
} elseif ($this->TABLECFG['col_left']) {
$kk = key($this->TABLECFG['col_left']);
$cmd = 'col_left';
} elseif ($this->TABLECFG['col_right']) {
$kk = key($this->TABLECFG['col_right']);
$cmd = 'col_right';
} elseif ($this->TABLECFG['row_remove']) {
$kk = key($this->TABLECFG['row_remove']);
$cmd = 'row_remove';
} elseif ($this->TABLECFG['row_add']) {
$kk = key($this->TABLECFG['row_add']);
$cmd = 'row_add';
} elseif ($this->TABLECFG['row_top']) {
$kk = key($this->TABLECFG['row_top']);
$cmd = 'row_top';
} elseif ($this->TABLECFG['row_bottom']) {
$kk = key($this->TABLECFG['row_bottom']);
$cmd = 'row_bottom';
} elseif ($this->TABLECFG['row_up']) {
$kk = key($this->TABLECFG['row_up']);
$cmd = 'row_up';
} elseif ($this->TABLECFG['row_down']) {
$kk = key($this->TABLECFG['row_down']);
$cmd = 'row_down';
} else {
$kk = '';
$cmd = '';
}
if ($cmd && MathUtility::canBeInterpretedAsInteger($kk)) {
if (strpos($cmd, 'row_') === 0) {
switch ($cmd) {
case 'row_remove':
unset($this->TABLECFG['c'][$kk]);
break;
case 'row_add':
for ($a = 1; $a <= $this->numNewRows; $a++) {
// Checking if set: The point is that any new row between existing rows
// will be TRUE after one row is added while if rows are added in the bottom
// of the table there will be no existing rows to stop the addition of new rows
// which means it will add up to $this->numNewRows rows then.
if (!isset($this->TABLECFG['c'][$kk + $a])) {
$this->TABLECFG['c'][$kk + $a] = [];
} else {
break;
}
}
break;
case 'row_top':
$this->TABLECFG['c'][1] = $this->TABLECFG['c'][$kk];
unset($this->TABLECFG['c'][$kk]);
break;
case 'row_bottom':
$this->TABLECFG['c'][10000000] = $this->TABLECFG['c'][$kk];
unset($this->TABLECFG['c'][$kk]);
break;
case 'row_up':
$this->TABLECFG['c'][$kk - 3] = $this->TABLECFG['c'][$kk];
unset($this->TABLECFG['c'][$kk]);
break;
case 'row_down':
$this->TABLECFG['c'][$kk + 3] = $this->TABLECFG['c'][$kk];
unset($this->TABLECFG['c'][$kk]);
break;
}
ksort($this->TABLECFG['c']);
}
if (strpos($cmd, 'col_') === 0) {
foreach ($this->TABLECFG['c'] as $cAK => $value) {
switch ($cmd) {
case 'col_remove':
unset($this->TABLECFG['c'][$cAK][$kk]);
break;
case 'col_add':
$this->TABLECFG['c'][$cAK][$kk + 1] = '';
break;
case 'col_start':
$this->TABLECFG['c'][$cAK][1] = $this->TABLECFG['c'][$cAK][$kk];
unset($this->TABLECFG['c'][$cAK][$kk]);
break;
case 'col_end':
$this->TABLECFG['c'][$cAK][1000000] = $this->TABLECFG['c'][$cAK][$kk];
unset($this->TABLECFG['c'][$cAK][$kk]);
break;
case 'col_left':
$this->TABLECFG['c'][$cAK][$kk - 3] = $this->TABLECFG['c'][$cAK][$kk];
unset($this->TABLECFG['c'][$cAK][$kk]);
break;
case 'col_right':
$this->TABLECFG['c'][$cAK][$kk + 3] = $this->TABLECFG['c'][$cAK][$kk];
unset($this->TABLECFG['c'][$cAK][$kk]);
break;
}
ksort($this->TABLECFG['c'][$cAK]);
}
}
}
// Convert line breaks to <br /> tags:
foreach ($this->TABLECFG['c'] as $a => $value) {
foreach ($this->TABLECFG['c'][$a] as $b => $value2) {
$this->TABLECFG['c'][$a][$b] = str_replace(
[CR, LF],
['', '<br />'],
$this->TABLECFG['c'][$a][$b]
);
}
}
}
/**
* Converts the input array to a configuration code string
*
* @param array $cfgArr Array of table configuration (follows the input structure from the table wizard POST form)
* @return string The array converted into a string with line-based configuration.
* @see configurationStringToArray()
*/
protected function configurationArrayToString(array $cfgArr): string
{
$inLines = [];
// Traverse the elements of the table wizard and transform the settings into configuration code.
foreach ($cfgArr as $valueA) {
$thisLine = [];
foreach ($valueA as $valueB) {
$thisLine[] = $this->tableParsing_quote
. str_replace($this->tableParsing_delimiter, '', $valueB) . $this->tableParsing_quote;
}
$inLines[] = implode($this->tableParsing_delimiter, $thisLine);
}
// Finally, implode the lines into a string:
return implode(LF, $inLines);
}
/**
* Converts the input configuration code string into an array
*
* @param string $configurationCode Configuration code
* @param int $columns Default number of columns
* @return array Configuration array
* @see configurationArrayToString()
*/
protected function configurationStringToArray(string $configurationCode, int $columns): array
{
// Explode lines in the configuration code - each line is a table row.
$tableLines = explode(LF, $configurationCode);
// Setting number of columns
// auto...
if (!$columns && trim($tableLines[0])) {
$columns = count(explode($this->tableParsing_delimiter, $tableLines[0]));
}
$columns = $columns ?: 4;
// Traverse the number of table elements:
$configurationArray = [];
foreach ($tableLines as $key => $value) {
// Initialize:
$valueParts = explode($this->tableParsing_delimiter, $value);
// Traverse columns:
for ($a = 0; $a < $columns; $a++) {
if ($this->tableParsing_quote
&& $valueParts[$a][0] === $this->tableParsing_quote
&& substr($valueParts[$a], -1, 1) === $this->tableParsing_quote
) {
$valueParts[$a] = substr(trim($valueParts[$a]), 1, -1);
}
$configurationArray[$key][$a] = (string)$valueParts[$a];
}
}
return $configurationArray;
}
}