TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Form\FormDataCompiler;
|
||||
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
|
||||
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
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.
|
||||
*/
|
||||
#[AsController]
|
||||
class AddController
|
||||
{
|
||||
/**
|
||||
* If set, the DataHandler class is loaded and used to add the returning ID to the parent record.
|
||||
*/
|
||||
protected int $processDataFlag = 0;
|
||||
|
||||
/**
|
||||
* Create new record -pid (pos/neg). If blank, return immediately
|
||||
*/
|
||||
protected int $pid = 0;
|
||||
|
||||
/**
|
||||
* The parent table we are working on.
|
||||
*/
|
||||
protected string $table = '';
|
||||
|
||||
/**
|
||||
* Loaded with the created id of a record FormEngine returns ...
|
||||
*/
|
||||
protected int $id = 0;
|
||||
|
||||
/**
|
||||
* Wizard parameters, coming from TCEforms linking to the wizard.
|
||||
*/
|
||||
protected array $P = [];
|
||||
|
||||
/**
|
||||
* Information coming back from the FormEngine script, telling what the table/id was of the newly created record.
|
||||
*/
|
||||
protected string $returnEditConf = '';
|
||||
|
||||
public function __construct(
|
||||
private readonly FormDataCompiler $formDataCompiler,
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->init($request);
|
||||
|
||||
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
|
||||
$input = [
|
||||
'request' => $request,
|
||||
'tableName' => $this->P['table'],
|
||||
'vanillaUid' => (int)$this->P['uid'],
|
||||
'command' => 'edit',
|
||||
];
|
||||
$result = $this->formDataCompiler->compile($input, GeneralUtility::makeInstance(TcaDatabaseRecord::class));
|
||||
$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)) {
|
||||
$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']];
|
||||
$currentFlexFormValueByPath = ArrayUtility::getValueByPath($currentFlexFormData, $this->P['flexFormPath']);
|
||||
|
||||
// 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']] = ArrayUtility::setValueByPath([], $this->P['flexFormPath'], $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 (array|int|string $item): string {
|
||||
// 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'], $request));
|
||||
}
|
||||
|
||||
// Redirecting to FormEngine with instructions to create a new record
|
||||
// AND when closing to return back with information about that records ID etc.
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
|
||||
'returnEditConf' => 1,
|
||||
'edit[' . $this->P['params']['table'] . '][' . $this->pid . ']' => 'new',
|
||||
// @todo add module context to wizard/add routes and set module context here
|
||||
'returnUrl' => $normalizedParams->getRequestUri(),
|
||||
]);
|
||||
|
||||
return new RedirectResponse($redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization of the class.
|
||||
*/
|
||||
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'] ?? '';
|
||||
// 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 = FormEngineUtility::getTCEFORM_TSconfig(
|
||||
$this->P['table'],
|
||||
is_array($record) ? $record : ['pid' => (int)$this->P['params']['pid']]
|
||||
);
|
||||
// Set [params][pid]
|
||||
if (str_starts_with($this->P['params']['pid'], '###') && str_ends_with($this->P['params']['pid'], '###')) {
|
||||
$keyword = substr($this->P['params']['pid'], 3, -3);
|
||||
$this->pid = str_starts_with($keyword, 'PAGE_TSCONFIG_')
|
||||
? (int)$TSconfig[$this->P['field']][$keyword]
|
||||
: (int)$TSconfig['_' . $keyword];
|
||||
} else {
|
||||
$this->pid = (int)$this->P['params']['pid'];
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
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\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
#[AsController]
|
||||
class EditController
|
||||
{
|
||||
protected const JAVASCRIPT_HELPER = 'EXT:backend/Resources/Public/JavaScript/helper.js';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* HTML markup to close the open window.
|
||||
*/
|
||||
protected string $closeWindow;
|
||||
|
||||
public function __construct(
|
||||
private readonly FlexFormTools $flexFormTools,
|
||||
private readonly TcaSchemaFactory $tcaSchemaFactory,
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->closeWindow = sprintf(
|
||||
'<script %s></script>',
|
||||
GeneralUtility::implodeAttributes([
|
||||
'src' => (string)PathUtility::getSystemResourceUri(self::JAVASCRIPT_HELPER, $request),
|
||||
'data-action' => 'window.close',
|
||||
], true)
|
||||
);
|
||||
$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;
|
||||
|
||||
return $this->processRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process request function
|
||||
* Makes a header-location redirect to an edit form IF POSSIBLE from the passed data - otherwise the window will
|
||||
* just close.
|
||||
*/
|
||||
protected function processRequest(): ResponseInterface
|
||||
{
|
||||
if ($this->doClose) {
|
||||
return new HtmlResponse($this->closeWindow);
|
||||
}
|
||||
// Initialize:
|
||||
$table = $this->P['table'];
|
||||
$field = $this->P['field'];
|
||||
$schema = $this->tcaSchemaFactory->get($table);
|
||||
|
||||
if (empty($this->P['flexFormDataStructureIdentifier'])) {
|
||||
// If there is not flex data structure identifier, field config is found in globals
|
||||
$config = $schema->getField($field)->getConfiguration();
|
||||
} else {
|
||||
// If there is a flex data structure identifier, parse that data structure and
|
||||
// fetch config defined by given flex path
|
||||
$dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($this->P['flexFormDataStructureIdentifier'], $schema);
|
||||
$config = ArrayUtility::getValueByPath($dataStructure, $this->P['flexFormDataStructurePath']);
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$urlParameters = [
|
||||
'returnUrl' => (string)$this->uriBuilder->buildUriFromRoute('wizard_edit', ['doClose' => 1]),
|
||||
];
|
||||
|
||||
// Detecting the various allowed field type setups and acting accordingly.
|
||||
if ($config['type'] === 'select'
|
||||
&& !($config['MM'] ?? false)
|
||||
&& (int)($config['maxitems'] ?? 0) <= 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
|
||||
// Note: no 'module' context here, since we're opening in a popup
|
||||
$url = $this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
|
||||
if (!empty($config['type'])
|
||||
&& !empty($this->P['currentSelectedValues'])
|
||||
&& (
|
||||
$config['type'] === 'select' && !empty($config['foreign_table'])
|
||||
|| $config['type'] === 'group' && !empty($config['allowed'])
|
||||
)
|
||||
) {
|
||||
// MULTIPLE VALUES:
|
||||
// Init settings:
|
||||
$allowedTables = $config['type'] === 'group' ? $config['allowed'] : $config['foreign_table'];
|
||||
// Selecting selected values into an array:
|
||||
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
|
||||
$relationHandler->start($this->P['currentSelectedValues'], $allowedTables);
|
||||
$value = $relationHandler->getValueArray(true);
|
||||
// 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 = $this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
return new HtmlResponse($this->closeWindow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
#[AsController]
|
||||
readonly class ImageManipulationController
|
||||
{
|
||||
public function __construct(
|
||||
protected BackendViewFactory $backendViewFactory,
|
||||
protected HashService $hashService,
|
||||
protected ResourceFactory $resourceFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the HTML for the wizard inside the modal
|
||||
*/
|
||||
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 = $this->resourceFactory->getFileObject($fileUid);
|
||||
} catch (FileDoesNotExistException $e) {
|
||||
}
|
||||
}
|
||||
$view = $this->backendViewFactory->create($request);
|
||||
$view->assignMultiple([
|
||||
'image' => $image,
|
||||
'cropVariants' => $parsedBody['cropVariants'],
|
||||
]);
|
||||
return new HtmlResponse($view->render('Form/ImageManipulationWizard'));
|
||||
}
|
||||
return new HtmlResponse('', 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if hmac signature is correct
|
||||
*
|
||||
* @param ServerRequestInterface $request the request with the POST parameters
|
||||
*/
|
||||
protected function isSignatureValid(ServerRequestInterface $request): bool
|
||||
{
|
||||
$token = $this->hashService->hmac($request->getParsedBody()['arguments'], 'ajax_wizard_image_manipulation');
|
||||
return hash_equals($token, $request->getParsedBody()['signature']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Script Class for redirecting the user to the Content > Records 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
|
||||
{
|
||||
/**
|
||||
* Injects the request object for the current request or sub request
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
// Wizard parameters, coming from FormEngine linking to this wizard.
|
||||
$parameters = $parsedBody['P'] ?? $queryParams['P'] ?? null;
|
||||
$id = $parsedBody['id'] ?? $queryParams['id'] ?? null;
|
||||
$table = $parameters['table'] ?? '';
|
||||
$origRow = BackendUtility::getRecord($table, $parameters['uid']);
|
||||
$tsConfig = FormEngineUtility::getTCEFORM_TSconfig($table, $origRow ?? ['pid' => $parameters['pid'] ?? 0]);
|
||||
|
||||
if (str_starts_with($parameters['params']['pid'], '###') && substr($parameters['params']['pid'], -3) === '###') {
|
||||
$keyword = substr($parameters['params']['pid'], 3, -3);
|
||||
if (str_starts_with($keyword, 'PAGE_TSCONFIG_')) {
|
||||
$pid = (int)$tsConfig[$parameters['field']][$keyword];
|
||||
} else {
|
||||
$pid = (int)$tsConfig['_' . $keyword];
|
||||
}
|
||||
} else {
|
||||
$pid = (int)$parameters['params']['pid'];
|
||||
}
|
||||
|
||||
if ((string)$id !== '') {
|
||||
// If pid is blank
|
||||
$redirectUrl = GeneralUtility::sanitizeLocalUrl($parameters['returnUrl'], $request);
|
||||
} else {
|
||||
// Otherwise, show the list
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$requestUri = $normalizedParams->getRequestUri();
|
||||
$urlParameters = [];
|
||||
$urlParameters['id'] = $pid;
|
||||
$urlParameters['table'] = $parameters['params']['table'];
|
||||
$urlParameters['returnUrl'] = !empty($parameters['returnUrl'])
|
||||
? GeneralUtility::sanitizeLocalUrl($parameters['returnUrl'], $request)
|
||||
: $requestUri;
|
||||
$redirectUrl = (string)$uriBuilder->buildUriFromRoute('records', $urlParameters);
|
||||
}
|
||||
|
||||
return new RedirectResponse($redirectUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,779 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
|
||||
use TYPO3\CMS\Backend\Controller\Event\AfterPageColumnsSelectedForLocalizationEvent;
|
||||
use TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent;
|
||||
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
|
||||
use TYPO3\CMS\Backend\Localization\LocalizationHandlerInterface;
|
||||
use TYPO3\CMS\Backend\Localization\LocalizationHandlerRegistry;
|
||||
use TYPO3\CMS\Backend\Localization\LocalizationInstructions;
|
||||
use TYPO3\CMS\Backend\Localization\LocalizationMode;
|
||||
use TYPO3\CMS\Backend\Localization\LocalizationResult;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Http\Response;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Versioning\VersionState;
|
||||
|
||||
/**
|
||||
* LocalizationController handles the AJAX requests for the localization wizard.
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class LocalizationController
|
||||
{
|
||||
public function __construct(
|
||||
protected IconFactory $iconFactory,
|
||||
protected LocalizationRepository $localizationRepository,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected LocalizationHandlerInterface $localizationHandler,
|
||||
protected LocalizationHandlerRegistry $localizationHandlerRegistry,
|
||||
protected TcaSchemaFactory $schemaFactory,
|
||||
protected TranslationConfigurationProvider $translationConfigurationProvider,
|
||||
protected BackendLayoutView $backendLayoutView,
|
||||
protected ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get record information for localization wizard
|
||||
*/
|
||||
public function getRecord(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
if (!isset($params['recordType'], $params['recordUid'])) {
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
$recordType = $params['recordType'];
|
||||
$recordUid = (int)$params['recordUid'];
|
||||
|
||||
$record = BackendUtility::getRecord($recordType, $recordUid);
|
||||
if (!$record) {
|
||||
return new JsonResponse(null, 404);
|
||||
}
|
||||
|
||||
$schema = $this->schemaFactory->get($recordType);
|
||||
$recordTitle = BackendUtility::getRecordTitle($recordType, $record);
|
||||
$recordInfo = [
|
||||
'uid' => $record['uid'],
|
||||
'title' => BackendUtility::cropToTitleLength($recordTitle),
|
||||
'icon' => $this->iconFactory->getIconForRecord($recordType, $record, IconSize::SMALL)->getIdentifier(),
|
||||
'type' => $recordType,
|
||||
'typeName' => $schema->getTitle($this->getLanguageService()->sL(...)),
|
||||
];
|
||||
|
||||
return new JsonResponse($recordInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available localization handlers
|
||||
*
|
||||
* Returns handlers filtered by the localization context
|
||||
*/
|
||||
public function getHandlers(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
|
||||
try {
|
||||
$localizationInstructions = LocalizationInstructions::create($params);
|
||||
} catch (\ValueError) {
|
||||
return new JsonResponse(['error' => 'Invalid localization mode'], 400);
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Validate required parameters
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
// Get available handlers from registry
|
||||
$handlers = $this->localizationHandlerRegistry->getAvailableHandlers($localizationInstructions);
|
||||
|
||||
// Prepare handlers for JSON response with translated labels
|
||||
$result = [];
|
||||
foreach ($handlers as $handler) {
|
||||
$result[] = [
|
||||
'identifier' => $handler->getIdentifier(),
|
||||
'label' => $this->getLanguageService()->sL($handler->getLabel()),
|
||||
'description' => $this->getLanguageService()->sL($handler->getDescription()),
|
||||
'iconIdentifier' => $handler->getIconIdentifier(),
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available localization modes
|
||||
*/
|
||||
public function getModes(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
if (!isset($params['recordType'], $params['recordUid'], $params['targetLanguage'])) {
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
$recordType = $params['recordType'];
|
||||
$recordUid = (int)$params['recordUid'];
|
||||
$targetLanguage = (int)$params['targetLanguage'];
|
||||
|
||||
// For pages, use the recordUid directly as the page
|
||||
// For other record types, find the parent page
|
||||
if ($recordType === 'pages') {
|
||||
$page = $recordUid;
|
||||
} else {
|
||||
// Get the record to find its parent page
|
||||
$record = BackendUtility::getRecord($recordType, $recordUid);
|
||||
if (!$record) {
|
||||
return new JsonResponse(null, 404);
|
||||
}
|
||||
$page = (int)$record['pid'];
|
||||
}
|
||||
|
||||
// Get page record for permission checks
|
||||
$pageRecord = BackendUtility::readPageAccess($page, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
||||
if (!$pageRecord) {
|
||||
return new JsonResponse(null, 403);
|
||||
}
|
||||
|
||||
// Get available modes based on PageTSconfig
|
||||
$pageTsConfig = BackendUtility::getPagesTSconfig($page);
|
||||
$schema = $this->schemaFactory->get($recordType);
|
||||
if (!$schema->hasCapability(TcaSchemaCapability::Language)) {
|
||||
// Table is not language-aware
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
||||
|
||||
$availableModes = array_filter(
|
||||
LocalizationMode::cases(),
|
||||
static function (LocalizationMode $mode) use ($pageTsConfig, $languageCapability): bool {
|
||||
return match ($mode) {
|
||||
LocalizationMode::COPY => ($pageTsConfig['mod.']['web_layout.']['localization.']['enableCopy'] ?? true) && $languageCapability->hasTranslationSourceField(),
|
||||
LocalizationMode::TRANSLATE => (bool)($pageTsConfig['mod.']['web_layout.']['localization.']['enableTranslate'] ?? true),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Check if there are existing translations in the target language
|
||||
// If so, we need to ensure we don't mix localization modes
|
||||
// This check is only relevant for pages and tt_content records
|
||||
if ($recordType === 'pages' || $recordType === 'tt_content') {
|
||||
$existingMode = $this->detectExistingLocalizationMode($page, $targetLanguage);
|
||||
if ($existingMode !== null) {
|
||||
// Filter to only allow the existing mode
|
||||
$availableModes = array_filter(
|
||||
$availableModes,
|
||||
static fn(LocalizationMode $mode): bool => $mode === $existingMode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority (highest first)
|
||||
usort($availableModes, static fn(LocalizationMode $a, LocalizationMode $b): int => $b->getPriority() <=> $a->getPriority());
|
||||
|
||||
$modes = array_map(
|
||||
fn(LocalizationMode $mode): array => [
|
||||
'key' => $mode->value,
|
||||
'label' => $this->getLanguageService()->sL($mode->getLabel()),
|
||||
'description' => $this->getLanguageService()->sL($mode->getDescription()),
|
||||
'iconIdentifier' => $mode->getIconIdentifier(),
|
||||
],
|
||||
$availableModes
|
||||
);
|
||||
|
||||
return new JsonResponse($modes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all target languages available for translation (excluding default language)
|
||||
*/
|
||||
public function getTargets(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
if (!isset($params['recordType'], $params['recordUid'])) {
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
$recordType = $params['recordType'];
|
||||
$recordUid = (int)$params['recordUid'];
|
||||
|
||||
// For pages, use the recordUid directly as the page
|
||||
// For other record types, find the parent page
|
||||
if ($recordType === 'pages') {
|
||||
$page = $recordUid;
|
||||
} else {
|
||||
// Get the record to find its parent page
|
||||
$record = BackendUtility::getRecord($recordType, $recordUid);
|
||||
if (!$record) {
|
||||
return new JsonResponse(null, 404);
|
||||
}
|
||||
$page = (int)$record['pid'];
|
||||
}
|
||||
|
||||
// Get page record for permission checks
|
||||
$pageRecord = BackendUtility::readPageAccess($page, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
||||
if (!$pageRecord) {
|
||||
return new JsonResponse(null, 403);
|
||||
}
|
||||
|
||||
$systemLanguages = $this->translationConfigurationProvider->getSystemLanguages($page);
|
||||
$availableLanguages = [];
|
||||
foreach ($systemLanguages as $languageUid => $language) {
|
||||
// Exclude "All languages" (-1) and default language (0) for target language selection
|
||||
if ($languageUid !== -1 && $languageUid !== 0) {
|
||||
$availableLanguages[] = $language;
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonResponse($availableLanguages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source languages that have content and can be used as translation base
|
||||
*/
|
||||
public function getSources(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
if (!isset($params['recordType'], $params['recordUid'], $params['targetLanguage'])) {
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
$recordType = (string)$params['recordType'];
|
||||
$recordUid = (int)$params['recordUid'];
|
||||
$targetLanguage = (int)$params['targetLanguage'];
|
||||
|
||||
// For pages, use the recordUid directly as the page
|
||||
// For other record types, find the parent page
|
||||
if ($recordType === 'pages') {
|
||||
$page = $recordUid;
|
||||
} else {
|
||||
// Get the record to find its parent page
|
||||
$record = BackendUtility::getRecord($recordType, $recordUid);
|
||||
if (!$record) {
|
||||
return new JsonResponse(null, 404);
|
||||
}
|
||||
$page = (int)$record['pid'];
|
||||
}
|
||||
|
||||
// Get page record for permission checks
|
||||
$pageRecord = BackendUtility::readPageAccess($page, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
||||
if (!$pageRecord) {
|
||||
return new JsonResponse(null, 403);
|
||||
}
|
||||
|
||||
$systemLanguages = $this->translationConfigurationProvider->getSystemLanguages($page);
|
||||
$availableLanguages = [];
|
||||
|
||||
// Find all existing translations of the record
|
||||
$record = BackendUtility::getRecord($recordType, $recordUid);
|
||||
if ($record) {
|
||||
$existingLanguageUids = [0]; // Always include default language
|
||||
|
||||
// Check each system language to see if a translation exists
|
||||
foreach (array_keys($systemLanguages) as $languageUid) {
|
||||
if ($languageUid > 0) { // Skip default language (0) and "All languages" (-1)
|
||||
$translation = $this->localizationRepository->getRecordTranslation($recordType, $record, (int)$languageUid);
|
||||
if ($translation !== null) {
|
||||
$existingLanguageUids[] = $languageUid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($existingLanguageUids as $languageUid) {
|
||||
if ($languageUid !== $targetLanguage && isset($systemLanguages[$languageUid])) {
|
||||
$availableLanguages[] = $systemLanguages[$languageUid];
|
||||
}
|
||||
}
|
||||
|
||||
// For pages with existing translations in the target language, we need to restrict source languages
|
||||
// to prevent mixed translation origins (e.g., some content from language A, some from language B)
|
||||
if ($recordType === 'pages') {
|
||||
$availableLanguages = $this->filterSourceLanguagesForPage($recordUid, $targetLanguage, $availableLanguages);
|
||||
}
|
||||
}
|
||||
|
||||
// Language "All" should not appear as a source of translations (see bug 92757) and keys should be sequential
|
||||
$availableLanguages = array_values(
|
||||
array_filter($availableLanguages, static function (array $languageRecord): bool {
|
||||
return (int)$languageRecord['uid'] !== -1;
|
||||
})
|
||||
);
|
||||
|
||||
return new JsonResponse($availableLanguages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page layout and records for localization
|
||||
*/
|
||||
public function getContent(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
if (!isset($params['pageUid'], $params['targetLanguage'], $params['sourceLanguage'])) {
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
$pageUid = (int)$params['pageUid'];
|
||||
$targetLanguage = (int)$params['targetLanguage'];
|
||||
$sourceLanguage = (int)$params['sourceLanguage'];
|
||||
|
||||
$records = [];
|
||||
$result = $this->localizationRepository->getRecordsToCopyDatabaseResult(
|
||||
$pageUid,
|
||||
$targetLanguage,
|
||||
$sourceLanguage,
|
||||
$this->getBackendUser()->workspace
|
||||
);
|
||||
|
||||
$flatRecords = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
BackendUtility::workspaceOL('tt_content', $row, $this->getBackendUser()->workspace, true);
|
||||
if (!$row || VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) {
|
||||
continue;
|
||||
}
|
||||
$colPos = $row['colPos'];
|
||||
|
||||
if (!$this->backendLayoutView->isCTypeAllowedInColPosByPage($row['CType'], $colPos, $pageUid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($records[$colPos])) {
|
||||
$records[$colPos] = [];
|
||||
}
|
||||
$recordTitle = BackendUtility::getRecordTitle('tt_content', $row);
|
||||
$records[$colPos][] = [
|
||||
'icon' => $this->iconFactory->getIconForRecord('tt_content', $row, IconSize::SMALL)->getIdentifier(),
|
||||
'title' => BackendUtility::cropToTitleLength($recordTitle),
|
||||
'uid' => $row['uid'],
|
||||
];
|
||||
$flatRecords[] = $row;
|
||||
}
|
||||
|
||||
$columns = $this->getPageColumns($pageUid, $flatRecords, $params);
|
||||
$event = new AfterRecordSummaryForLocalizationEvent($records, $columns);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
// Get the backend layout structure for visual representation
|
||||
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageUid);
|
||||
$layoutStructure = $this->buildLayoutStructure($backendLayout, $event->getColumns(), $event->getRecords());
|
||||
|
||||
return new JsonResponse([
|
||||
'layout' => $layoutStructure,
|
||||
]);
|
||||
}
|
||||
|
||||
public function localize(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getParsedBody();
|
||||
|
||||
if (!isset(
|
||||
$params['recordType'],
|
||||
$params['recordUid'],
|
||||
$params['data']['sourceLanguage'],
|
||||
$params['data']['targetLanguage'],
|
||||
$params['data']['localizationMode']
|
||||
)) {
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
$recordType = $params['recordType'];
|
||||
$recordUid = (int)$params['recordUid'];
|
||||
$sourceLanguage = (int)$params['data']['sourceLanguage'];
|
||||
$targetLanguage = (int)$params['data']['targetLanguage'];
|
||||
$modeIdentifier = $params['data']['localizationMode'];
|
||||
$handlerIdentifier = $params['data']['localizationHandler'] ?? 'manual';
|
||||
|
||||
// Prepare Additional Data
|
||||
$additionalData = $params['data'];
|
||||
unset($additionalData['sourceLanguage']);
|
||||
unset($additionalData['targetLanguage']);
|
||||
unset($additionalData['localizationMode']);
|
||||
unset($additionalData['localizationHandler']);
|
||||
|
||||
// Validate that the mode exists
|
||||
$mode = LocalizationMode::tryFrom($modeIdentifier);
|
||||
if ($mode === null) {
|
||||
$response = new Response('php://temp', 400, ['Content-Type' => 'application/json; charset=utf-8']);
|
||||
$response->getBody()->write('Invalid localization mode "' . $modeIdentifier . '" called.');
|
||||
return $response;
|
||||
}
|
||||
|
||||
try {
|
||||
$localizationInstructions = new LocalizationInstructions(
|
||||
$recordType,
|
||||
$recordUid,
|
||||
$sourceLanguage,
|
||||
$targetLanguage,
|
||||
$mode,
|
||||
$additionalData
|
||||
);
|
||||
} catch (\ValueError) {
|
||||
return new JsonResponse(['error' => 'Invalid localization mode'], 400);
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Validate required parameters
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
// Process the localization using the handler
|
||||
try {
|
||||
// Get the handler from the registry or fall back to the default handler
|
||||
if ($this->localizationHandlerRegistry->hasHandler($handlerIdentifier)) {
|
||||
$handler = $this->localizationHandlerRegistry->getHandler($handlerIdentifier);
|
||||
} else {
|
||||
$handler = $this->localizationHandler;
|
||||
}
|
||||
|
||||
// Use the handler to process the localization with the selected mode
|
||||
$result = $handler->processLocalization($localizationInstructions);
|
||||
} catch (\Exception $e) {
|
||||
$result = LocalizationResult::error([$e->getMessage()]);
|
||||
}
|
||||
return new JsonResponse($result->jsonSerialize());
|
||||
}
|
||||
|
||||
private function getPageColumns(int $page, array $flatRecords, array $params): array
|
||||
{
|
||||
$columns = [];
|
||||
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($page);
|
||||
|
||||
foreach ($backendLayout->getUsedColumns() as $columnPos => $columnLabel) {
|
||||
$columns[$columnPos] = $this->getLanguageService()->sL($columnLabel);
|
||||
}
|
||||
|
||||
$event = new AfterPageColumnsSelectedForLocalizationEvent($columns, [], $backendLayout, $flatRecords, $params);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
return $event->getColumns();
|
||||
}
|
||||
|
||||
private function buildLayoutStructure($backendLayout, array $columns, array $records): array
|
||||
{
|
||||
// Calculate total elements across all columns
|
||||
$elementCount = 0;
|
||||
foreach ($records as $colPos => $columnRecords) {
|
||||
if (is_array($columnRecords)) {
|
||||
$elementCount += count($columnRecords);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$backendLayout) {
|
||||
// Create a simple single-row layout when no backend layout is available
|
||||
$layoutColumns = [];
|
||||
foreach ($columns as $colPos => $columnLabel) {
|
||||
$layoutColumns[] = [
|
||||
'position' => (int)$colPos,
|
||||
'label' => $columnLabel,
|
||||
'records' => $records[$colPos] ?? [],
|
||||
'colspan' => 1,
|
||||
'rowspan' => 1,
|
||||
'identifier' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'layout',
|
||||
'title' => 'Default Layout',
|
||||
'identifier' => 'default',
|
||||
'colCount' => count($columns),
|
||||
'rowCount' => 1,
|
||||
'elementCount' => $elementCount,
|
||||
'rows' => [
|
||||
[
|
||||
'columns' => $layoutColumns,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$structure = $backendLayout->getStructure();
|
||||
$layoutRows = [];
|
||||
|
||||
if (!empty($structure['__config']['backend_layout.']['rows.'])) {
|
||||
$rows = $structure['__config']['backend_layout.']['rows.'];
|
||||
ksort($rows);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$layoutColumns = [];
|
||||
|
||||
if (!empty($row['columns.'])) {
|
||||
foreach ($row['columns.'] as $column) {
|
||||
if (!isset($column['colPos'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$colPos = (int)$column['colPos'];
|
||||
$layoutColumns[] = [
|
||||
'position' => $colPos,
|
||||
'label' => $columns[$colPos] ?? $column['name'],
|
||||
'records' => $records[$colPos] ?? [],
|
||||
'colspan' => (int)($column['colspan'] ?? 1),
|
||||
'rowspan' => (int)($column['rowspan'] ?? 1),
|
||||
'identifier' => $column['identifier'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$layoutRows[] = [
|
||||
'columns' => $layoutColumns,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'layout',
|
||||
'title' => $backendLayout->getTitle(),
|
||||
'identifier' => $backendLayout->getIdentifier(),
|
||||
'colCount' => $backendLayout->getColCount(),
|
||||
'rowCount' => $backendLayout->getRowCount(),
|
||||
'elementCount' => $elementCount,
|
||||
'rows' => $layoutRows,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter available source languages for page translations based on existing content
|
||||
*
|
||||
* For pages, when translations already exist in the target language with content assigned,
|
||||
* we need to ensure that new content is only translated from the same source language(s)
|
||||
* as the existing content to avoid creating mixed translations in terms of language origin.
|
||||
*
|
||||
* @param int $pageUid The page UID being translated
|
||||
* @param int $targetLanguage The target language ID
|
||||
* @param array $availableLanguages All available source languages (to be filtered)
|
||||
* @return array Filtered available language configurations
|
||||
*/
|
||||
private function filterSourceLanguagesForPage(int $pageUid, int $targetLanguage, array $availableLanguages): array
|
||||
{
|
||||
// Check if a page translation exists in the target language
|
||||
$pageTranslation = $this->localizationRepository->getPageTranslations($pageUid, [$targetLanguage], $this->getBackendUser()->workspace);
|
||||
if ($pageTranslation === []) {
|
||||
return $availableLanguages;
|
||||
}
|
||||
|
||||
// Get source languages used by existing content in the target language
|
||||
// Note: Content elements are stored on the original page with sys_language_uid set to the target language
|
||||
$usedSourceLanguages = $this->getUsedSourceLanguagesForPage($pageUid, $targetLanguage);
|
||||
|
||||
if (empty($usedSourceLanguages)) {
|
||||
return $availableLanguages;
|
||||
}
|
||||
|
||||
// Filter to only allow source languages already in use
|
||||
return array_filter(
|
||||
$availableLanguages,
|
||||
static fn(array $language): bool => isset($usedSourceLanguages[(int)$language['uid']])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source languages used by existing content on a page
|
||||
*
|
||||
* @param int $pageUid The page UID
|
||||
* @param int $targetLanguage The target language ID
|
||||
* @return array<int, true> Map of source language UIDs that are in use
|
||||
*/
|
||||
private function getUsedSourceLanguagesForPage(int $pageUid, int $targetLanguage): array
|
||||
{
|
||||
$schema = $this->schemaFactory->get('tt_content');
|
||||
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
||||
|
||||
if (!$languageCapability->hasTranslationSourceField()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$languageField = $languageCapability->getLanguageField()->getName();
|
||||
$translationSourceField = $languageCapability->getTranslationSourceField()->getName();
|
||||
|
||||
// Get all l10n_source UIDs from translated content
|
||||
$queryBuilder = $this->connectionPool
|
||||
->getQueryBuilderForTable('tt_content');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
|
||||
|
||||
$result = $queryBuilder
|
||||
->select($translationSourceField)
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
$languageField,
|
||||
$queryBuilder->createNamedParameter($targetLanguage, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->gt(
|
||||
$translationSourceField,
|
||||
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery();
|
||||
|
||||
// Collect unique source UIDs
|
||||
$sourceUids = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$sourceUid = (int)$row[$translationSourceField];
|
||||
$sourceUids[$sourceUid] = $sourceUid;
|
||||
}
|
||||
|
||||
if (empty($sourceUids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get the language of all source records
|
||||
$sourceQueryBuilder = $this->connectionPool
|
||||
->getQueryBuilderForTable('tt_content');
|
||||
$sourceQueryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
|
||||
$sourceResult = $sourceQueryBuilder
|
||||
->select($languageField)
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$sourceQueryBuilder->expr()->in(
|
||||
'uid',
|
||||
$sourceQueryBuilder->createNamedParameter($sourceUids, Connection::PARAM_INT_ARRAY)
|
||||
)
|
||||
)
|
||||
->groupBy($languageField)
|
||||
->executeQuery();
|
||||
|
||||
$usedSourceLanguages = [];
|
||||
while ($row = $sourceResult->fetchAssociative()) {
|
||||
$sourceLanguageUid = (int)$row[$languageField];
|
||||
$usedSourceLanguages[$sourceLanguageUid] = true;
|
||||
}
|
||||
|
||||
return $usedSourceLanguages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the localization mode used by existing translations on a page
|
||||
*
|
||||
* This method checks if there are existing content elements in the target language
|
||||
* and determines whether they were created using COPY (free) or TRANSLATE (connected) mode.
|
||||
* This prevents mixing different localization modes on the same page, which would lead to
|
||||
* inconsistent translation workflows.
|
||||
*
|
||||
* Note: Pages themselves are always created in connected mode (using 'localize' command),
|
||||
* so we check the content elements on the page to determine the actual localization mode.
|
||||
*
|
||||
* The distinction is made by checking the translation origin pointer field obtained from the
|
||||
* schema's language capability:
|
||||
* - TRANSLATE mode (connected): Records have translation origin pointer > 0 (linked to source language)
|
||||
* - COPY mode (free): Records have translation origin pointer = 0 (independent copies)
|
||||
*
|
||||
* @param int $pageId The page ID to check
|
||||
* @param int $targetLanguage The target language ID
|
||||
* @return LocalizationMode|null The detected mode, or null if no translations exist
|
||||
*/
|
||||
private function detectExistingLocalizationMode(int $pageId, int $targetLanguage): ?LocalizationMode
|
||||
{
|
||||
// Get the TCA schema to determine the correct field names
|
||||
$schema = $this->schemaFactory->get('tt_content');
|
||||
|
||||
if (!$schema->hasCapability(TcaSchemaCapability::Language)) {
|
||||
// Table is not language-aware
|
||||
return null;
|
||||
}
|
||||
|
||||
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
||||
$transOrigPointerField = $languageCapability->getTranslationOriginPointerField()->getName();
|
||||
$languageField = $languageCapability->getLanguageField()->getName();
|
||||
|
||||
// Check content elements on the page, as pages themselves are always connected
|
||||
// but their content determines the actual localization mode being used
|
||||
$queryBuilder = $this->connectionPool
|
||||
->getQueryBuilderForTable('tt_content');
|
||||
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
|
||||
|
||||
// Select only the translation pointer field to determine the mode
|
||||
$result = $queryBuilder
|
||||
->select($transOrigPointerField)
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
$languageField,
|
||||
$queryBuilder->createNamedParameter($targetLanguage, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery();
|
||||
|
||||
$hasRecords = false;
|
||||
$hasConnected = false;
|
||||
|
||||
// Iterate through results to detect the mode
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$hasRecords = true;
|
||||
$transOrigPointer = (int)($row[$transOrigPointerField] ?? 0);
|
||||
|
||||
// If we find any connected record (transOrigPointer > 0), return TRANSLATE immediately
|
||||
// This prevents mixing modes even if there are also free mode records
|
||||
if ($transOrigPointer > 0) {
|
||||
$hasConnected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No records found
|
||||
if (!$hasRecords) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If any connected record exists, return TRANSLATE mode
|
||||
// Otherwise, all records are free mode, return COPY
|
||||
return $hasConnected ? LocalizationMode::TRANSLATE : LocalizationMode::COPY;
|
||||
}
|
||||
|
||||
private function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Form\FormDataCompiler;
|
||||
use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
|
||||
use TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEffectivePid;
|
||||
use TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseParentPageRow;
|
||||
use TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowInitializeNew;
|
||||
use TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseUniqueUidNewRow;
|
||||
use TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseUserPermissionCheck;
|
||||
use TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca;
|
||||
use TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfig;
|
||||
use TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems;
|
||||
use TYPO3\CMS\Backend\Form\FormDataProvider\UserTsConfig;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Controller providing AJAX endpoints for page wizard functionality.
|
||||
* Handles fetching doktypes, page details, and processed field values
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
final readonly class PageWizardController
|
||||
{
|
||||
public function __construct(
|
||||
private IconFactory $iconFactory,
|
||||
private FormDataCompiler $formDataCompiler
|
||||
) {}
|
||||
|
||||
public function getDoktypesAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$position = $request->getQueryParams()['data']['position'] ?? [];
|
||||
$pageUid = (int)($position['pageUid'] ?? 0);
|
||||
$insertPosition = $position['insertPosition'] ?? 'inside';
|
||||
|
||||
$parentPageUid = $insertPosition === 'inside'
|
||||
? $pageUid
|
||||
: (BackendUtility::getRecord('pages', $pageUid, 'pid')['pid'] ?? null);
|
||||
|
||||
$backendUser = $this->getBackendUser();
|
||||
$parentPage = BackendUtility::readPageAccess((int)$parentPageUid, $backendUser->getPagePermsClause(Permission::PAGE_NEW));
|
||||
if (!$parentPage) {
|
||||
return new JsonResponse(null, 403);
|
||||
}
|
||||
|
||||
$formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
|
||||
$formDataGroup->setProviderList([
|
||||
InitializeProcessedTca::class,
|
||||
DatabaseParentPageRow::class,
|
||||
DatabaseUserPermissionCheck::class,
|
||||
DatabaseEffectivePid::class,
|
||||
UserTsConfig::class,
|
||||
PageTsConfig::class,
|
||||
DatabaseRowInitializeNew::class,
|
||||
DatabaseUniqueUidNewRow::class,
|
||||
TcaSelectItems::class,
|
||||
]);
|
||||
|
||||
$doktypes = $this->formDataCompiler
|
||||
->compile(
|
||||
[
|
||||
'command' => 'new',
|
||||
'request' => $request,
|
||||
'tableName' => 'pages',
|
||||
'vanillaUid' => $parentPageUid,
|
||||
],
|
||||
$formDataGroup
|
||||
)['processedTca']['columns']['doktype']['config']['items'] ?? [];
|
||||
|
||||
$result = [];
|
||||
foreach ($doktypes as $doktype) {
|
||||
$result[] = [
|
||||
'value' => $doktype['value'] ?? '',
|
||||
'label' => $doktype['label'] ?? '',
|
||||
'icon' => $doktype['icon'] ?? '',
|
||||
'description' => $doktype['description'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse($result, 200);
|
||||
}
|
||||
|
||||
public function getPageDetailAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
$pageUid = $params['pageUid'] ?? null;
|
||||
|
||||
if ($pageUid === null) {
|
||||
return new JsonResponse(['error' => 'Missing required query parameter: pageUid'], 400);
|
||||
}
|
||||
|
||||
if ((int)$pageUid === 0) {
|
||||
return new JsonResponse([
|
||||
'uid' => 0,
|
||||
'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? 'TYPO3',
|
||||
'icon' => 'apps-pagetree-root',
|
||||
]);
|
||||
}
|
||||
|
||||
$page = BackendUtility::readPageAccess((int)$pageUid, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
||||
|
||||
if (!$page) {
|
||||
return new JsonResponse(null, 403);
|
||||
}
|
||||
|
||||
$recordInfo = [
|
||||
'uid' => $page['uid'],
|
||||
'title' => $page['title'],
|
||||
'icon' => $this->iconFactory->getIconForRecord('pages', $page, IconSize::SMALL)->getIdentifier(),
|
||||
];
|
||||
|
||||
return new JsonResponse($recordInfo);
|
||||
}
|
||||
|
||||
public function getProcessedValueAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
$fields = $params['fields'] ?? [];
|
||||
$pageUid = (int)($params['pageUid'] ?? 0);
|
||||
|
||||
$page = BackendUtility::readPageAccess($pageUid, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
||||
if (!$page) {
|
||||
return new JsonResponse(null, 403);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($fields as $fieldName => $value) {
|
||||
$result[$fieldName] = BackendUtility::getProcessedValue('pages', $fieldName, $value, 0, false, false, 0, true, $pageUid);
|
||||
}
|
||||
|
||||
return new JsonResponse($result);
|
||||
}
|
||||
|
||||
private function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Schema\Capability\RootLevelCapability;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchema;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Security\RawValue;
|
||||
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.
|
||||
*/
|
||||
#[AsController]
|
||||
readonly class SuggestWizardController
|
||||
{
|
||||
public function __construct(
|
||||
private FlexFormTools $flexFormTools,
|
||||
private TcaSchemaFactory $tcaSchemaFactory,
|
||||
private ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ajax handler for the "suggest" feature in FormEngine.
|
||||
*
|
||||
* @throws \RuntimeException for incomplete or invalid arguments
|
||||
*/
|
||||
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 = $parsedBody['dataStructureIdentifier'] ?? '';
|
||||
$flexFormSheetName = $parsedBody['flexFormSheetName'] ?? null;
|
||||
$flexFormFieldName = $parsedBody['flexFormFieldName'] ?? null;
|
||||
$flexFormContainerName = $parsedBody['flexFormContainerName'] ?? null;
|
||||
$flexFormContainerFieldName = $parsedBody['flexFormContainerFieldName'] ?? null;
|
||||
$recordType = (string)($parsedBody['recordTypeValue'] ?? '') ?: null;
|
||||
$schema = $this->tcaSchemaFactory->get($tableName);
|
||||
|
||||
// Determine TCA config of field
|
||||
if (empty($dataStructureIdentifier)) {
|
||||
// Normal columns field
|
||||
$fieldInformation = $schema->getField($fieldName);
|
||||
$fieldConfig = $fieldInformation->getConfiguration();
|
||||
$fieldNameInPageTsConfig = $fieldName;
|
||||
|
||||
// With possible columnsOverrides
|
||||
// @todo Validate if we can move this fallback recordType determination, should be do-able in v13?!
|
||||
if ($recordType === null) {
|
||||
$recordType = BackendUtility::getTCAtypeValue(
|
||||
$tableName,
|
||||
BackendUtility::getRecord($tableName, $uid) ?? [],
|
||||
true
|
||||
);
|
||||
}
|
||||
if ($recordType !== null && $schema->hasSubSchema($recordType)) {
|
||||
$fieldConfig = $schema->getSubSchema($recordType)->getField($fieldName)->getConfiguration();
|
||||
}
|
||||
} else {
|
||||
// A flex-form field
|
||||
$dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier, $schema);
|
||||
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]['config'])
|
||||
) {
|
||||
throw new \RuntimeException(
|
||||
'Specified path ' . $flexFormFieldName . ' not found in flex form data structure',
|
||||
1480609491
|
||||
);
|
||||
}
|
||||
$fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]['config'];
|
||||
} else {
|
||||
$fieldNameInPageTsConfig = $flexFormContainerFieldName;
|
||||
if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]
|
||||
['el'][$flexFormContainerName]
|
||||
['el'][$flexFormContainerFieldName]['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]['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 (!$this->tcaSchemaFactory->has($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(',', (string)$fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
|
||||
}
|
||||
if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
|
||||
$connection = $this->connectionPool->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'] = QueryHelper::quoteDatabaseIdentifiers($this->connectionPool->getConnectionForTable($queryTable), 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 = (int)($config['maxItemsInResultList'] ?? 10);
|
||||
$maxItems = min(count($resultRows), $maxItems);
|
||||
|
||||
array_splice($resultRows, $maxItems);
|
||||
return new JsonResponse(array_values($resultRows));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current backend user is allowed to access the given table, based on the schema capabilities.
|
||||
*/
|
||||
protected function currentBackendUserMayAccessTable(TcaSchema $schema): bool
|
||||
{
|
||||
if ($this->getBackendUser()->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the user is no admin, they may not access admin-only tables
|
||||
if ($schema->hasCapability(TcaSchemaCapability::AccessAdminOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var RootLevelCapability $rootLevelCapability */
|
||||
$rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel);
|
||||
|
||||
// allow access to root level pages if security restrictions should be bypassed
|
||||
return $rootLevelCapability->canAccessRecordsOnRootLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected function getConfigurationForTable(string $queryTable, array $wizardConfig, array $TSconfig, string $table, string $field): array
|
||||
{
|
||||
$config = (array)($wizardConfig['default'] ?? []);
|
||||
|
||||
if (is_array($wizardConfig[$queryTable] ?? null)) {
|
||||
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.'] ?? null)) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($globalSuggestTsConfig['default.']));
|
||||
}
|
||||
|
||||
if (is_array($globalSuggestTsConfig[$queryTable . '.'] ?? null)) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($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.'] ?? null)) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($currentFieldSuggestTsConfig['default.']));
|
||||
}
|
||||
|
||||
if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'] ?? null)) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($currentFieldSuggestTsConfig[$queryTable . '.']));
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given field configuration for the tables that should be used for querying and returns them as an
|
||||
* array.
|
||||
*/
|
||||
protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig): array
|
||||
{
|
||||
$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
|
||||
/** @var TcaSchema $schema */
|
||||
foreach ($this->tcaSchemaFactory->all() as $tableName => $schema) {
|
||||
if ($schema->hasCapability(TcaSchemaCapability::HideInUi)) {
|
||||
continue;
|
||||
}
|
||||
if ($this->currentBackendUserMayAccessTable($schema)) {
|
||||
$queryTables[] = $tableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (isset($fieldConfig['foreign_table'])) {
|
||||
// use the foreign table
|
||||
$queryTables = [$fieldConfig['foreign_table']];
|
||||
}
|
||||
|
||||
return $queryTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps user functions in the configuration array as a `RawValue` object,
|
||||
* to be asserted later when actually calling `GeneralUtility::callUserFunction`.
|
||||
*/
|
||||
protected function substituteRawValues(array $config): array
|
||||
{
|
||||
if (!empty($config['renderFunc'])) {
|
||||
$config['renderFunc'] = new RawValue($config['renderFunc']);
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected function getWhereClause(array $fieldConfig): string
|
||||
{
|
||||
if (!isset($fieldConfig['foreign_table'], $fieldConfig['foreign_table_where'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// strip ORDER BY clause
|
||||
return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Wizard\WizardProviderInterface;
|
||||
use TYPO3\CMS\Backend\Wizard\WizardProviderRegistry;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
final readonly class WizardController
|
||||
{
|
||||
public function __construct(
|
||||
private WizardProviderRegistry $wizardProviderRegistry
|
||||
) {}
|
||||
|
||||
public function getConfigurationAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return new JsonResponse(
|
||||
$this->getProviderByRequest($request)
|
||||
->getConfiguration($request)
|
||||
->jsonSerialize()
|
||||
);
|
||||
}
|
||||
|
||||
public function submitDataAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return new JsonResponse(
|
||||
$this->getProviderByRequest($request)
|
||||
->handleSubmit($request)
|
||||
->jsonSerialize()
|
||||
);
|
||||
}
|
||||
|
||||
private function getProviderByRequest(ServerRequestInterface $request): WizardProviderInterface
|
||||
{
|
||||
return $this->wizardProviderRegistry->getProvider($request->getQueryParams()['mode'] ?? '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user