[FEATURE] Add inline AJAX validation for TCA type slug
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / FormSlugAjaxController.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Backend\Controller;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use Psr\Http\Message\ResponseInterface;
20 use Psr\Http\Message\ServerRequestInterface;
21 use TYPO3\CMS\Core\DataHandling\SlugHelper;
22 use TYPO3\CMS\Core\Http\JsonResponse;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25 /**
26 * Handle FormEngine AJAX calls for Slug validation and sanitization
27 */
28 class FormSlugAjaxController extends AbstractFormEngineAjaxController
29 {
30
31 /**
32 * Validates a given slug against the site and give a suggestion when it's already in use
33 *
34 * For new records this will look like this:
35 * - If "slug" field is empty, take the other fields, and generate the slug based on the sent fields.
36 * - JS: adapt the "placeholder" value only, as on save the field will be filled with the value via DataHandler
37 * - If "slug" field is not empty (= "unlocked" and manually typed in)
38 * - sanitize the slug
39 * - If 'uniqueInSite' is set check if it's unique for the site
40 * - If not unique propose another slug and return this with the flag hasConflicts = true
41 * - If 'uniqueInPid' is set check if it's unique for the pid
42 * - If not unique propose another slug and return this with the flag hasConflicts = true
43 *
44 * For existing records:
45 * - sanitize the slug
46 * - If 'uniqueInSite' is set check if it's unique for the site
47 * - If not unique propose another slug and return this with the flag hasConflicts = true
48 * - If 'uniqueInPid' is set check if it's unique for the pid
49 * - If not unique propose another slug and return this with the flag hasConflicts = true
50 * - If the slug has changed from the existing database record (@todo)
51 * - Show a message that the old URL will stop working (possibly add a redirect via checkbox)
52 * - If the page has subpages, show a warning that the subpages WILL NOT BE MODIFIED and keep the OLD url
53 *
54 * @param ServerRequestInterface $request
55 * @return ResponseInterface
56 */
57 public function suggestAction(ServerRequestInterface $request): ResponseInterface
58 {
59 $this->checkRequest($request);
60
61 $queryParameters = $request->getParsedBody() ?? [];
62 $values = $queryParameters['values'];
63 $autoGeneration = $queryParameters['autoGeneration'] === 'true' ? true : false;
64 $tableName = $queryParameters['tableName'];
65 $pid = (int)$queryParameters['pageId'];
66 $recordId = (int)$queryParameters['recordId'];
67 $languageId = (int)$queryParameters['language'];
68 $fieldName = $queryParameters['fieldName'];
69
70 $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'] ?? [];
71 if (empty($fieldConfig)) {
72 throw new \RuntimeException(
73 'No valid field configuration for table ' . $tableName . ' field name ' . $fieldName . ' found.',
74 1535379534
75 );
76 }
77
78 $evalInfo = !empty($fieldConfig['eval']) ? GeneralUtility::trimExplode(',', $fieldConfig['eval'], true) : [];
79 $hasToBeUniqueInSite = in_array('uniqueInSite', $evalInfo, true);
80 $hasToBeUniqueInPid = in_array('uniqueInPid', $evalInfo, true);
81
82 $hasConflict = false;
83
84 $slug = GeneralUtility::makeInstance(SlugHelper::class, $tableName, $fieldName, $fieldConfig);
85 if ($autoGeneration) {
86 // New page - Feed incoming values to generator
87 $recordData = $values;
88 $recordData['pid'] = $pid;
89 $recordData[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = $languageId;
90 $proposal = $slug->generate($recordData, $pid);
91 } else {
92 // Existing record - Fetch full record and only validate against the new "slug" field.
93 $proposal = $slug->sanitize($values['manual']);
94 }
95
96 if ($hasToBeUniqueInSite && !$slug->isUniqueInSite($proposal, $recordId, $pid, $languageId)) {
97 $hasConflict = true;
98 $proposal = $slug->buildSlugForUniqueInSite($proposal, $recordId, $pid, $languageId);
99 }
100 if ($hasToBeUniqueInPid && !$slug->isUniqueInPid($proposal, $recordId, $pid, $languageId)) {
101 $hasConflict = true;
102 $proposal = $slug->buildSlugForUniqueInPid($proposal, $recordId, $pid, $languageId);
103 }
104
105 return new JsonResponse([
106 'hasConflicts' => !$autoGeneration && $hasConflict,
107 'manual' => $values['manual'] ?? '',
108 'proposal' => $proposal,
109 ]);
110 }
111
112 /**
113 * @param ServerRequestInterface $request
114 * @return bool
115 */
116 protected function checkRequest(ServerRequestInterface $request): bool
117 {
118 $queryParameters = $request->getParsedBody() ?? [];
119 $expectedHash = GeneralUtility::hmac(
120 implode(
121 '',
122 [
123 $queryParameters['tableName'],
124 $queryParameters['pageId'],
125 $queryParameters['recordId'],
126 $queryParameters['language'],
127 $queryParameters['fieldName'],
128 $queryParameters['command'],
129 ]
130 ),
131 __CLASS__
132 );
133 if (!hash_equals($expectedHash, $queryParameters['signature'])) {
134 throw new \InvalidArgumentException(
135 'HMAC could not be verified',
136 1535137045
137 );
138 }
139 return true;
140 }
141 }