186 lines
7.8 KiB
PHP
186 lines
7.8 KiB
PHP
<?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;
|
|
}
|
|
}
|