[TASK] Render wizards of FormEngine elements only if needed
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Element / InputLinkElement.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\Element;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Imaging\Icon;
19 use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
20 use TYPO3\CMS\Core\LinkHandling\LinkService;
21 use TYPO3\CMS\Core\Localization\LanguageService;
22 use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
23 use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
24 use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
25 use TYPO3\CMS\Core\Resource\File;
26 use TYPO3\CMS\Core\Resource\Folder;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Utility\MathUtility;
29 use TYPO3\CMS\Core\Utility\StringUtility;
30 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
31
32 /**
33 * Link input element.
34 *
35 * Shows current link and the link popup.
36 */
37 class InputLinkElement extends AbstractFormElement
38 {
39 /**
40 * Default field information enabled for this element.
41 *
42 * @var array
43 */
44 protected $defaultFieldInformation = [
45 'tcaDescription' => [
46 'renderType' => 'tcaDescription',
47 ],
48 ];
49
50 /**
51 * Default field controls render the link icon
52 *
53 * @var array
54 */
55 protected $defaultFieldControl = [
56 'linkPopup' => [
57 'renderType' => 'linkPopup',
58 'options' => []
59 ],
60 ];
61
62 /**
63 * Default field wizards enabled for this element.
64 *
65 * @var array
66 */
67 protected $defaultFieldWizard = [
68 'localizationStateSelector' => [
69 'renderType' => 'localizationStateSelector',
70 ],
71 'otherLanguageContent' => [
72 'renderType' => 'otherLanguageContent',
73 'after' => [
74 'localizationStateSelector'
75 ],
76 ],
77 'defaultLanguageDifferences' => [
78 'renderType' => 'defaultLanguageDifferences',
79 'after' => [
80 'otherLanguageContent',
81 ],
82 ],
83 ];
84
85 /**
86 * This will render a single-line input form field, possibly with various control/validation features
87 *
88 * @return array As defined in initializeResultArray() of AbstractNode
89 */
90 public function render()
91 {
92 $languageService = $this->getLanguageService();
93
94 $table = $this->data['tableName'];
95 $fieldName = $this->data['fieldName'];
96 $row = $this->data['databaseRow'];
97 $parameterArray = $this->data['parameterArray'];
98 $resultArray = $this->initializeResultArray();
99 $config = $parameterArray['fieldConf']['config'];
100
101 $itemValue = $parameterArray['itemFormElValue'];
102 $evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
103 $size = MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
104 $width = (int)$this->formMaxWidth($size);
105 $nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']');
106
107 $fieldInformationResult = $this->renderFieldInformation();
108 $fieldInformationHtml = $fieldInformationResult['html'];
109 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
110
111 if ($config['readOnly']) {
112 // Early return for read only fields
113 $html = [];
114 $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
115 $html[] = $fieldInformationHtml;
116 $html[] = '<div class="form-wizards-wrap">';
117 $html[] = '<div class="form-wizards-element">';
118 $html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
119 $html[] = '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
120 $html[] = '</div>';
121 $html[] = '</div>';
122 $html[] = '</div>';
123 $html[] = '</div>';
124 $resultArray['html'] = implode(LF, $html);
125 return $resultArray;
126 }
127
128 // @todo: The whole eval handling is a mess and needs refactoring
129 foreach ($evalList as $func) {
130 // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
131 // @todo: keyword like "date", or a class reference. The global registration could be dropped then
132 // Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
133 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
134 if (class_exists($func)) {
135 $evalObj = GeneralUtility::makeInstance($func);
136 if (method_exists($evalObj, 'deevaluateFieldValue')) {
137 $_params = [
138 'value' => $itemValue
139 ];
140 $itemValue = $evalObj->deevaluateFieldValue($_params);
141 }
142 if (method_exists($evalObj, 'returnFieldJS')) {
143 $resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($func) . ']'
144 . ' = function(value) {' . $evalObj->returnFieldJS() . '};';
145 }
146 }
147 }
148 }
149
150 $attributes = [
151 'value' => '',
152 'id' => StringUtility::getUniqueId('formengine-input-'),
153 'class' => implode(' ', [
154 'form-control',
155 't3js-clearable',
156 't3js-form-field-inputlink-input',
157 'hidden',
158 'hasDefaultValue',
159 ]),
160 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
161 'data-formengine-input-params' => json_encode([
162 'field' => $parameterArray['itemFormElName'],
163 'evalList' => implode(',', $evalList)
164 ]),
165 'data-formengine-input-name' => $parameterArray['itemFormElName'],
166 ];
167
168 $maxLength = $config['max'] ?? 0;
169 if ((int)$maxLength > 0) {
170 $attributes['maxlength'] = (int)$maxLength;
171 }
172 if (!empty($config['placeholder'])) {
173 $attributes['placeholder'] = trim($config['placeholder']);
174 }
175 if (isset($config['autocomplete'])) {
176 $attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
177 }
178
179 $valuePickerHtml = [];
180 if (isset($config['valuePicker']['items']) && is_array($config['valuePicker']['items'])) {
181 $mode = $config['valuePicker']['mode'] ?? '';
182 $itemName = $parameterArray['itemFormElName'];
183 $fieldChangeFunc = $parameterArray['fieldChangeFunc'];
184 if ($mode === 'append') {
185 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
186 . '.value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
187 } elseif ($mode === 'prepend') {
188 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
189 . '.value+=\'\'+this.options[this.selectedIndex].value';
190 } else {
191 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
192 . '.value=this.options[this.selectedIndex].value';
193 }
194 $valuePickerHtml[] = '<select';
195 $valuePickerHtml[] = ' class="form-control tceforms-select tceforms-wizardselect"';
196 $valuePickerHtml[] = ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"';
197 $valuePickerHtml[] = '>';
198 $valuePickerHtml[] = '<option></option>';
199 foreach ($config['valuePicker']['items'] as $item) {
200 $valuePickerHtml[] = '<option value="' . htmlspecialchars($item[1]) . '">' . htmlspecialchars($languageService->sL($item[0])) . '</option>';
201 }
202 $valuePickerHtml[] = '</select>';
203 }
204
205 $fieldWizardResult = $this->renderFieldWizard();
206 $fieldWizardHtml = $fieldWizardResult['html'];
207 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
208
209 $fieldControlResult = $this->renderFieldControl();
210 $fieldControlHtml = $fieldControlResult['html'];
211 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
212
213 $linkExplanation = $this->getLinkExplanation($itemValue ?: '');
214 $explanation = htmlspecialchars($linkExplanation['text']);
215 $toggleButtonTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.toggleLinkExplanation');
216
217 $expansionHtml = [];
218 $expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
219 $expansionHtml[] = '<div class="form-wizards-wrap">';
220 $expansionHtml[] = '<div class="form-wizards-element">';
221 $expansionHtml[] = '<div class="input-group t3js-form-field-inputlink">';
222 $expansionHtml[] = '<span class="input-group-addon">' . $linkExplanation['icon'] . '</span>';
223 $expansionHtml[] = '<input class="form-control form-field-inputlink-explanation t3js-form-field-inputlink-explanation" data-toggle="tooltip" data-title="' . $explanation . '" value="' . $explanation . '" readonly>';
224 $expansionHtml[] = '<input type="text"' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
225 $expansionHtml[] = '<span class="input-group-btn">';
226 $expansionHtml[] = '<button class="btn btn-default t3js-form-field-inputlink-explanation-toggle" type="button" title="' . htmlspecialchars($toggleButtonTitle) . '">';
227 $expansionHtml[] = $this->iconFactory->getIcon('actions-version-workspaces-preview-link', Icon::SIZE_SMALL)->render();
228 $expansionHtml[] = '</button>';
229 $expansionHtml[] = '</span>';
230 $expansionHtml[] = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
231 $expansionHtml[] = '</div>';
232 $expansionHtml[] = '</div>';
233 if (!empty($valuePickerHtml) || !empty($fieldControlHtml)) {
234 $expansionHtml[] = '<div class="form-wizards-items-aside">';
235 $expansionHtml[] = '<div class="btn-group">';
236 $expansionHtml[] = implode(LF, $valuePickerHtml);
237 $expansionHtml[] = $fieldControlHtml;
238 $expansionHtml[] = '</div>';
239 $expansionHtml[] = '</div>';
240 }
241 $expansionHtml[] = '<div class="form-wizards-items-bottom">';
242 $expansionHtml[] = $linkExplanation['additionalAttributes'];
243 $expansionHtml[] = $fieldWizardHtml;
244 $expansionHtml[] = '</div>';
245 $expansionHtml[] = '</div>';
246 $expansionHtml[] = '</div>';
247 $expansionHtml = implode(LF, $expansionHtml);
248
249 $fullElement = $expansionHtml;
250 if ($this->hasNullCheckboxButNoPlaceholder()) {
251 $checked = $itemValue !== null ? ' checked="checked"' : '';
252 $fullElement = [];
253 $fullElement[] = '<div class="t3-form-field-disable"></div>';
254 $fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
255 $fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
256 $fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
257 $fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
258 $fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
259 $fullElement[] = '</label>';
260 $fullElement[] = '</div>';
261 $fullElement[] = $expansionHtml;
262 $fullElement = implode(LF, $fullElement);
263 } elseif ($this->hasNullCheckboxWithPlaceholder()) {
264 $checked = $itemValue !== null ? ' checked="checked"' : '';
265 $placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
266 $disabled = '';
267 $fallbackValue = 0;
268 if (strlen($placeholder) > 0) {
269 $shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
270 if ($placeholder !== $shortenedPlaceholder) {
271 $overrideLabel = sprintf(
272 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
273 '<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
274 );
275 } else {
276 $overrideLabel = sprintf(
277 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
278 htmlspecialchars($placeholder)
279 );
280 }
281 } else {
282 $overrideLabel = $languageService->sL(
283 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
284 );
285 }
286 $fullElement = [];
287 $fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
288 $fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
289 $fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="' . $fallbackValue . '" />';
290 $fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . $disabled . ' />';
291 $fullElement[] = $overrideLabel;
292 $fullElement[] = '</label>';
293 $fullElement[] = '</div>';
294 $fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
295 $fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
296 $fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . $shortenedPlaceholder . '" />';
297 $fullElement[] = '</div>';
298 $fullElement[] = '</div>';
299 $fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
300 $fullElement[] = $expansionHtml;
301 $fullElement[] = '</div>';
302 $fullElement = implode(LF, $fullElement);
303 }
304
305 $resultArray['html'] = '<div class="formengine-field-item t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
306 return $resultArray;
307 }
308
309 /**
310 * @param string $itemValue
311 * @return array
312 */
313 protected function getLinkExplanation(string $itemValue): array
314 {
315 if (empty($itemValue)) {
316 return [];
317 }
318 $data = ['text' => '', 'icon' => ''];
319 $typolinkService = GeneralUtility::makeInstance(TypoLinkCodecService::class);
320 $linkParts = $typolinkService->decode($itemValue);
321 $linkService = GeneralUtility::makeInstance(LinkService::class);
322
323 try {
324 $linkData = $linkService->resolve($linkParts['url']);
325 } catch (FileDoesNotExistException $e) {
326 return $data;
327 } catch (FolderDoesNotExistException $e) {
328 return $data;
329 } catch (UnknownLinkHandlerException $e) {
330 return $data;
331 } catch (InvalidPathException $e) {
332 return $data;
333 }
334
335 // Resolving the TypoLink parts (class, title, params)
336 $additionalAttributes = [];
337 foreach ($linkParts as $key => $value) {
338 if ($key === 'url') {
339 continue;
340 }
341 if ($value) {
342 switch ($key) {
343 case 'class':
344 $label = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:class');
345 break;
346 case 'title':
347 $label = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:title');
348 break;
349 case 'additionalParams':
350 $label = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:params');
351 break;
352 default:
353 $label = $key;
354 }
355
356 $additionalAttributes[] = '<span><strong>' . htmlspecialchars($label) . ': </strong> ' . htmlspecialchars($value) . '</span>';
357 }
358 }
359
360 // Resolve the actual link
361 switch ($linkData['type']) {
362 case LinkService::TYPE_PAGE:
363 $pageRecord = BackendUtility::readPageAccess($linkData['pageuid'], '1=1');
364 // Is this a real page
365 if ($pageRecord['uid']) {
366 $data = [
367 'text' => $pageRecord['_thePathFull'] . '[' . $pageRecord['uid'] . ']',
368 'icon' => $this->iconFactory->getIconForRecord('pages', $pageRecord, Icon::SIZE_SMALL)->render()
369 ];
370 }
371 break;
372 case LinkService::TYPE_EMAIL:
373 $data = [
374 'text' => $linkData['email'],
375 'icon' => $this->iconFactory->getIcon('content-elements-mailform', Icon::SIZE_SMALL)->render()
376 ];
377 break;
378 case LinkService::TYPE_URL:
379 $data = [
380 'text' => $this->getDomainByUrl($linkData['url']),
381 'icon' => $this->iconFactory->getIcon('apps-pagetree-page-shortcut-external', Icon::SIZE_SMALL)->render()
382
383 ];
384 break;
385 case LinkService::TYPE_FILE:
386 /** @var File $file */
387 $file = $linkData['file'];
388 if ($file) {
389 $data = [
390 'text' => $file->getPublicUrl(),
391 'icon' => $this->iconFactory->getIconForFileExtension($file->getExtension(), Icon::SIZE_SMALL)->render()
392 ];
393 }
394 break;
395 case LinkService::TYPE_FOLDER:
396 /** @var Folder $folder */
397 $folder = $linkData['folder'];
398 if ($folder) {
399 $data = [
400 'text' => $folder->getPublicUrl(),
401 'icon' => $this->iconFactory->getIcon('apps-filetree-folder-default', Icon::SIZE_SMALL)->render()
402 ];
403 }
404 break;
405 case LinkService::TYPE_RECORD:
406 $table = $this->data['pageTsConfig']['TCEMAIN.']['linkHandler.'][$linkData['identifier'] . '.']['configuration.']['table'];
407 $record = BackendUtility::getRecord($table, $linkData['uid']);
408 if ($record) {
409 $recordTitle = BackendUtility::getRecordTitle($table, $record);
410 $tableTitle = $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
411 $data = [
412 'text' => sprintf('%s [%s:%d]', $recordTitle, $tableTitle, $linkData['uid']),
413 'icon' => $this->iconFactory->getIconForRecord($table, $record, Icon::SIZE_SMALL)->render(),
414 ];
415 } else {
416 $icon = $GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default'];
417 if (empty($icon)) {
418 $icon = 'tcarecords-' . $table . '-default';
419 }
420 $data = [
421 'text' => sprintf('%s', $linkData['uid']),
422 'icon' => $this->iconFactory->getIcon('tcarecords-' . $table . '-default', Icon::SIZE_SMALL, 'overlay-missing')->render(),
423 ];
424 }
425 break;
426 default:
427 // Please note that this hook is preliminary and might change, as this element could become its own
428 // TCA type in the future
429 if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler'][$linkData['type']])) {
430 $linkBuilder = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler'][$linkData['type']]);
431 $data = $linkBuilder->getFormData($linkData, $linkParts, $this->data, $this);
432 } else {
433 $data = [
434 'text' => 'not implemented type ' . $linkData['type'],
435 'icon' => ''
436 ];
437 }
438 }
439
440 $data['additionalAttributes'] = '<div class="help-block">' . implode(' - ', $additionalAttributes) . '</div>';
441 return $data;
442 }
443
444 /**
445 * @param string $uriString
446 *
447 * @return string
448 */
449 protected function getDomainByUrl(string $uriString): string
450 {
451 $data = parse_url($uriString);
452 return $data['host'] ?? '';
453 }
454
455 /**
456 * @return LanguageService
457 */
458 protected function getLanguageService()
459 {
460 return $GLOBALS['LANG'];
461 }
462 }