TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<?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;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
|
||||
use TYPO3\CMS\Core\DataHandling\SlugHelper;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle FormEngine AJAX calls for Slug validation and sanitization
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
readonly class FormSlugAjaxController extends AbstractFormEngineAjaxController
|
||||
{
|
||||
public function __construct(
|
||||
private Context $context,
|
||||
private HashService $hashService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates a given slug against the site and give a suggestion when it's already in use
|
||||
*
|
||||
* For new records this will look like this:
|
||||
* - If "slug" field is empty, take the other fields, and generate the slug based on the sent fields.
|
||||
* - JS: adapt the "placeholder" value only, as on save the field will be filled with the value via DataHandler
|
||||
* - If "slug" field is not empty (= "unlocked" and manually typed in)
|
||||
* - sanitize the slug
|
||||
* - If 'uniqueInSite' is set check if it's unique for the site
|
||||
* - If not unique propose another slug and return this with the flag hasConflicts = true
|
||||
* - If 'uniqueInPid' is set check if it's unique for the pid
|
||||
* - If not unique propose another slug and return this with the flag hasConflicts = true
|
||||
*
|
||||
* For existing records:
|
||||
* - sanitize the slug
|
||||
* - If 'uniqueInSite' is set check if it's unique for the site
|
||||
* - If not unique propose another slug and return this with the flag hasConflicts = true
|
||||
* - If 'uniqueInPid' is set check if it's unique for the pid
|
||||
* - If not unique propose another slug and return this with the flag hasConflicts = true
|
||||
* - If the slug has changed from the existing database record (@todo)
|
||||
* - Show a message that the old URL will stop working (possibly add a redirect via checkbox)
|
||||
* - If the page has subpages, show a warning that the subpages WILL NOT BE MODIFIED and keep the OLD url
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function suggestAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->checkRequest($request);
|
||||
|
||||
$queryParameters = $request->getParsedBody() ?? [];
|
||||
$values = $queryParameters['values'];
|
||||
$mode = $queryParameters['mode'];
|
||||
$tableName = (string)($queryParameters['tableName'] ?? '');
|
||||
$pid = (int)$queryParameters['pageId'];
|
||||
$parentPageId = (int)$queryParameters['parentPageId'];
|
||||
$recordId = (int)$queryParameters['recordId'];
|
||||
$languageId = (int)$queryParameters['language'];
|
||||
$fieldName = $queryParameters['fieldName'];
|
||||
|
||||
$fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'] ?? [];
|
||||
$row = (array)BackendUtility::getRecord($tableName, $recordId);
|
||||
$recordType = BackendUtility::getTCAtypeValue($tableName, $row, true);
|
||||
if ($recordType !== null) {
|
||||
$columnsOverridesConfigOfField = $GLOBALS['TCA'][$tableName]['types'][$recordType]['columnsOverrides'][$fieldName]['config'] ?? null;
|
||||
if ($columnsOverridesConfigOfField) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($fieldConfig, $columnsOverridesConfigOfField);
|
||||
}
|
||||
}
|
||||
if (empty($fieldConfig)) {
|
||||
throw new \RuntimeException(
|
||||
'No valid field configuration for table ' . $tableName . ' field name ' . $fieldName . ' found.',
|
||||
1535379534
|
||||
);
|
||||
}
|
||||
|
||||
$evalInfo = !empty($fieldConfig['eval']) ? GeneralUtility::trimExplode(',', $fieldConfig['eval'], true) : [];
|
||||
$hasToBeUniqueInDb = in_array('unique', $evalInfo, true);
|
||||
$hasToBeUniqueInSite = in_array('uniqueInSite', $evalInfo, true);
|
||||
$hasToBeUniqueInPid = in_array('uniqueInPid', $evalInfo, true);
|
||||
|
||||
$hasConflict = false;
|
||||
|
||||
$recordData = $values;
|
||||
if (!isset($recordData['uid'])) {
|
||||
$recordData['uid'] = $recordId;
|
||||
}
|
||||
$recordData['pid'] = $pid;
|
||||
if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
|
||||
$recordData[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = $languageId;
|
||||
}
|
||||
if ($tableName === 'pages' && empty($recordData['is_siteroot'])) {
|
||||
$recordData['is_siteroot'] = $row['is_siteroot'] ?? false;
|
||||
}
|
||||
|
||||
$workspaceId = $this->context->getPropertyFromAspect('workspace', 'id');
|
||||
$slug = GeneralUtility::makeInstance(SlugHelper::class, $tableName, $fieldName, $fieldConfig, $workspaceId);
|
||||
if ($mode === 'auto') {
|
||||
// New page - Feed incoming values to generator
|
||||
$proposal = $slug->generate($recordData, $pid);
|
||||
} elseif ($mode === 'recreate') {
|
||||
$proposal = $slug->generate($recordData, $parentPageId);
|
||||
} elseif ($mode === 'manual') {
|
||||
// Existing record - Fetch full record and only validate against the new "slug" field.
|
||||
$proposal = $slug->sanitize($values['manual']);
|
||||
} else {
|
||||
throw new \RuntimeException('mode must be either "auto", "recreate" or "manual"', 1535835666);
|
||||
}
|
||||
|
||||
$state = RecordStateFactory::forName($tableName)
|
||||
->fromArray($recordData, $pid, $recordId);
|
||||
if ($hasToBeUniqueInDb && !$slug->isUniqueInTable($proposal, $state)) {
|
||||
$hasConflict = true;
|
||||
$proposal = $slug->buildSlugForUniqueInTable($proposal, $state);
|
||||
}
|
||||
if ($hasToBeUniqueInSite && !$slug->isUniqueInSite($proposal, $state)) {
|
||||
$hasConflict = true;
|
||||
$proposal = $slug->buildSlugForUniqueInSite($proposal, $state);
|
||||
}
|
||||
if ($hasToBeUniqueInPid && !$slug->isUniqueInPid($proposal, $state)) {
|
||||
$hasConflict = true;
|
||||
$proposal = $slug->buildSlugForUniqueInPid($proposal, $state);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'hasConflicts' => $hasConflict,
|
||||
'manual' => $values['manual'] ?? '',
|
||||
'proposal' => $proposal,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function checkRequest(ServerRequestInterface $request): bool
|
||||
{
|
||||
$queryParameters = $request->getParsedBody() ?? [];
|
||||
$expectedHash = $this->hashService->hmac(
|
||||
implode(
|
||||
'',
|
||||
[
|
||||
$queryParameters['tableName'],
|
||||
$queryParameters['pageId'],
|
||||
$queryParameters['recordId'],
|
||||
$queryParameters['language'],
|
||||
$queryParameters['fieldName'],
|
||||
$queryParameters['command'],
|
||||
$queryParameters['parentPageId'],
|
||||
]
|
||||
),
|
||||
__CLASS__
|
||||
);
|
||||
if (!hash_equals($expectedHash, $queryParameters['signature'])) {
|
||||
throw new \InvalidArgumentException(
|
||||
'HMAC could not be verified',
|
||||
1535137045
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user