[BUGFIX] Fix several typos in php comments
[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 an 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 $fieldId = StringUtility::getUniqueId('formengine-input-');
151
152 $attributes = [
153 'value' => '',
154 'id' => $fieldId,
155 'class' => implode(' ', [
156 'form-control',
157 't3js-clearable',
158 't3js-form-field-inputlink-input',
159 'hidden',
160 'hasDefaultValue',
161 ]),
162 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
163 'data-formengine-input-params' => json_encode([
164 'field' => $parameterArray['itemFormElName'],
165 'evalList' => implode(',', $evalList)
166 ]),
167 'data-formengine-input-name' => $parameterArray['itemFormElName'],
168 ];
169
170 $maxLength = $config['max'] ?? 0;
171 if ((int)$maxLength > 0) {
172 $attributes['maxlength'] = (int)$maxLength;
173 }
174 if (!empty($config['placeholder'])) {
175 $attributes['placeholder'] = trim($config['placeholder']);
176 }
177 if (isset($config['autocomplete'])) {
178 $attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
179 }
180
181 $valuePickerHtml = [];
182 if (isset($config['valuePicker']['items']) && is_array($config['valuePicker']['items'])) {
183 $mode = $config['valuePicker']['mode'] ?? '';
184 $itemName = $parameterArray['itemFormElName'];
185 $fieldChangeFunc = $parameterArray['fieldChangeFunc'];
186 if ($mode === 'append') {
187 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
188 . '.value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
189 } elseif ($mode === 'prepend') {
190 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
191 . '.value+=\'\'+this.options[this.selectedIndex].value';
192 } else {
193 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
194 . '.value=this.options[this.selectedIndex].value';
195 }
196 $valuePickerHtml[] = '<select';
197 $valuePickerHtml[] = ' class="form-control tceforms-select tceforms-wizardselect"';
198 $valuePickerHtml[] = ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"';
199 $valuePickerHtml[] = '>';
200 $valuePickerHtml[] = '<option></option>';
201 foreach ($config['valuePicker']['items'] as $item) {
202 $valuePickerHtml[] = '<option value="' . htmlspecialchars($item[1]) . '">' . htmlspecialchars($languageService->sL($item[0])) . '</option>';
203 }
204 $valuePickerHtml[] = '</select>';
205 }
206
207 $fieldWizardResult = $this->renderFieldWizard();
208 $fieldWizardHtml = $fieldWizardResult['html'];
209 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
210
211 $fieldControlResult = $this->renderFieldControl();
212 $fieldControlHtml = $fieldControlResult['html'];
213 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
214
215 $linkExplanation = $this->getLinkExplanation($itemValue ?: '');
216 $explanation = htmlspecialchars($linkExplanation['text']);
217 $toggleButtonTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.toggleLinkExplanation');
218
219 $expansionHtml = [];
220 $expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
221 $expansionHtml[] = '<div class="form-wizards-wrap">';
222 $expansionHtml[] = '<div class="form-wizards-element">';
223 $expansionHtml[] = '<div class="input-group t3js-form-field-inputlink">';
224 $expansionHtml[] = '<span class="input-group-addon">' . $linkExplanation['icon'] . '</span>';
225 $expansionHtml[] = '<input class="form-control form-field-inputlink-explanation t3js-form-field-inputlink-explanation" data-toggle="tooltip" data-title="' . $explanation . '" value="' . $explanation . '" readonly>';
226 $expansionHtml[] = '<input type="text"' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
227 $expansionHtml[] = '<span class="input-group-btn">';
228 $expansionHtml[] = '<button class="btn btn-default t3js-form-field-inputlink-explanation-toggle" type="button" title="' . htmlspecialchars($toggleButtonTitle) . '">';
229 $expansionHtml[] = $this->iconFactory->getIcon('actions-version-workspaces-preview-link', Icon::SIZE_SMALL)->render();
230 $expansionHtml[] = '</button>';
231 $expansionHtml[] = '</span>';
232 $expansionHtml[] = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
233 $expansionHtml[] = '</div>';
234 $expansionHtml[] = '</div>';
235 if (!empty($valuePickerHtml) || !empty($fieldControlHtml)) {
236 $expansionHtml[] = '<div class="form-wizards-items-aside">';
237 $expansionHtml[] = '<div class="btn-group">';
238 $expansionHtml[] = implode(LF, $valuePickerHtml);
239 $expansionHtml[] = $fieldControlHtml;
240 $expansionHtml[] = '</div>';
241 $expansionHtml[] = '</div>';
242 }
243 $expansionHtml[] = '<div class="form-wizards-items-bottom">';
244 $expansionHtml[] = $linkExplanation['additionalAttributes'];
245 $expansionHtml[] = $fieldWizardHtml;
246 $expansionHtml[] = '</div>';
247 $expansionHtml[] = '</div>';
248 $expansionHtml[] = '</div>';
249 $expansionHtml = implode(LF, $expansionHtml);
250
251 $fullElement = $expansionHtml;
252 if ($this->hasNullCheckboxButNoPlaceholder()) {
253 $checked = $itemValue !== null ? ' checked="checked"' : '';
254 $fullElement = [];
255 $fullElement[] = '<div class="t3-form-field-disable"></div>';
256 $fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
257 $fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
258 $fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
259 $fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
260 $fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
261 $fullElement[] = '</label>';
262 $fullElement[] = '</div>';
263 $fullElement[] = $expansionHtml;
264 $fullElement = implode(LF, $fullElement);
265 } elseif ($this->hasNullCheckboxWithPlaceholder()) {
266 $checked = $itemValue !== null ? ' checked="checked"' : '';
267 $placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
268 $disabled = '';
269 $fallbackValue = 0;
270 if (strlen($placeholder) > 0) {
271 $shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
272 if ($placeholder !== $shortenedPlaceholder) {
273 $overrideLabel = sprintf(
274 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
275 '<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
276 );
277 } else {
278 $overrideLabel = sprintf(
279 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
280 htmlspecialchars($placeholder)
281 );
282 }
283 } else {
284 $overrideLabel = $languageService->sL(
285 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
286 );
287 }
288 $fullElement = [];
289 $fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
290 $fullElement[] = '<label for="' . $nullControlNameEscaped . '">';
291 $fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="' . $fallbackValue . '" />';
292 $fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . $disabled . ' />';
293 $fullElement[] = $overrideLabel;
294 $fullElement[] = '</label>';
295 $fullElement[] = '</div>';
296 $fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
297 $fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
298 $fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . $shortenedPlaceholder . '" />';
299 $fullElement[] = '</div>';
300 $fullElement[] = '</div>';
301 $fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
302 $fullElement[] = $expansionHtml;
303 $fullElement[] = '</div>';
304 $fullElement = implode(LF, $fullElement);
305 }
306
307 $resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Element/InputLinkElement' => '
308 function(InputLinkElement) {
309 new InputLinkElement(' . GeneralUtility::quoteJSvalue($fieldId) . ');
310 }'
311 ];
312 $resultArray['html'] = '<div class="formengine-field-item t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
313 return $resultArray;
314 }
315
316 /**
317 * @param string $itemValue
318 * @return array
319 */
320 protected function getLinkExplanation(string $itemValue): array
321 {
322 if (empty($itemValue)) {
323 return [];
324 }
325 $data = ['text' => '', 'icon' => ''];
326 $typolinkService = GeneralUtility::makeInstance(TypoLinkCodecService::class);
327 $linkParts = $typolinkService->decode($itemValue);
328 $linkService = GeneralUtility::makeInstance(LinkService::class);
329
330 try {
331 $linkData = $linkService->resolve($linkParts['url']);
332 } catch (FileDoesNotExistException $e) {
333 return $data;
334 } catch (FolderDoesNotExistException $e) {
335 return $data;
336 } catch (UnknownLinkHandlerException $e) {
337 return $data;
338 } catch (InvalidPathException $e) {
339 return $data;
340 }
341
342 // Resolving the TypoLink parts (class, title, params)
343 $additionalAttributes = [];
344 foreach ($linkParts as $key => $value) {
345 if ($key === 'url') {
346 continue;
347 }
348 if ($value) {
349 switch ($key) {
350 case 'class':
351 $label = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:class');
352 break;
353 case 'title':
354 $label = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:title');
355 break;
356 case 'additionalParams':
357 $label = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:params');
358 break;
359 default:
360 $label = $key;
361 }
362
363 $additionalAttributes[] = '<span><strong>' . htmlspecialchars($label) . ': </strong> ' . htmlspecialchars($value) . '</span>';
364 }
365 }
366
367 // Resolve the actual link
368 switch ($linkData['type']) {
369 case LinkService::TYPE_PAGE:
370 $pageRecord = BackendUtility::readPageAccess($linkData['pageuid'], '1=1');
371 // Is this a real page
372 if ($pageRecord['uid']) {
373 $data = [
374 'text' => $pageRecord['_thePathFull'] . '[' . $pageRecord['uid'] . ']',
375 'icon' => $this->iconFactory->getIconForRecord('pages', $pageRecord, Icon::SIZE_SMALL)->render()
376 ];
377 }
378 break;
379 case LinkService::TYPE_EMAIL:
380 $data = [
381 'text' => $linkData['email'],
382 'icon' => $this->iconFactory->getIcon('content-elements-mailform', Icon::SIZE_SMALL)->render()
383 ];
384 break;
385 case LinkService::TYPE_URL:
386 $data = [
387 'text' => $this->getDomainByUrl($linkData['url']),
388 'icon' => $this->iconFactory->getIcon('apps-pagetree-page-shortcut-external', Icon::SIZE_SMALL)->render()
389
390 ];
391 break;
392 case LinkService::TYPE_FILE:
393 /** @var File $file */
394 $file = $linkData['file'];
395 if ($file) {
396 $data = [
397 'text' => $file->getPublicUrl(),
398 'icon' => $this->iconFactory->getIconForFileExtension($file->getExtension(), Icon::SIZE_SMALL)->render()
399 ];
400 }
401 break;
402 case LinkService::TYPE_FOLDER:
403 /** @var Folder $folder */
404 $folder = $linkData['folder'];
405 if ($folder) {
406 $data = [
407 'text' => $folder->getPublicUrl(),
408 'icon' => $this->iconFactory->getIcon('apps-filetree-folder-default', Icon::SIZE_SMALL)->render()
409 ];
410 }
411 break;
412 case LinkService::TYPE_RECORD:
413 $table = $this->data['pageTsConfig']['TCEMAIN.']['linkHandler.'][$linkData['identifier'] . '.']['configuration.']['table'];
414 $record = BackendUtility::getRecord($table, $linkData['uid']);
415 if ($record) {
416 $recordTitle = BackendUtility::getRecordTitle($table, $record);
417 $tableTitle = $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
418 $data = [
419 'text' => sprintf('%s [%s:%d]', $recordTitle, $tableTitle, $linkData['uid']),
420 'icon' => $this->iconFactory->getIconForRecord($table, $record, Icon::SIZE_SMALL)->render(),
421 ];
422 } else {
423 $data = [
424 'text' => sprintf('%s', $linkData['uid']),
425 'icon' => $this->iconFactory->getIcon('tcarecords-' . $table . '-default', Icon::SIZE_SMALL, 'overlay-missing')->render(),
426 ];
427 }
428 break;
429 default:
430 // Please note that this hook is preliminary and might change, as this element could become its own
431 // TCA type in the future
432 if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler'][$linkData['type']])) {
433 $linkBuilder = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler'][$linkData['type']]);
434 $data = $linkBuilder->getFormData($linkData, $linkParts, $this->data, $this);
435 } else {
436 $data = [
437 'text' => 'not implemented type ' . $linkData['type'],
438 'icon' => ''
439 ];
440 }
441 }
442
443 $data['additionalAttributes'] = '<div class="help-block">' . implode(' - ', $additionalAttributes) . '</div>';
444 return $data;
445 }
446
447 /**
448 * @param string $uriString
449 *
450 * @return string
451 */
452 protected function getDomainByUrl(string $uriString): string
453 {
454 $data = parse_url($uriString);
455 return $data['host'] ?? '';
456 }
457
458 /**
459 * @return LanguageService
460 */
461 protected function getLanguageService()
462 {
463 return $GLOBALS['LANG'];
464 }
465 }