5fe64b0caf9f422406c47e4633939bc634179e62
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Element / AbstractFormElement.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\Clipboard\Clipboard;
18 use TYPO3\CMS\Backend\Form\AbstractNode;
19 use TYPO3\CMS\Backend\Form\DatabaseFileIconsHookInterface;
20 use TYPO3\CMS\Backend\Form\FormDataCompiler;
21 use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
22 use TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems;
23 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
24 use TYPO3\CMS\Backend\Form\NodeFactory;
25 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
26 use TYPO3\CMS\Backend\Form\Wizard\SuggestWizard;
27 use TYPO3\CMS\Backend\Form\Wizard\ValueSliderWizard;
28 use TYPO3\CMS\Backend\Utility\BackendUtility;
29 use TYPO3\CMS\Core\Imaging\Icon;
30 use TYPO3\CMS\Core\Imaging\IconFactory;
31 use TYPO3\CMS\Core\Utility\ArrayUtility;
32 use TYPO3\CMS\Core\Utility\GeneralUtility;
33 use TYPO3\CMS\Core\Utility\MathUtility;
34 use TYPO3\CMS\Core\Utility\StringUtility;
35 use TYPO3\CMS\Lang\LanguageService;
36
37 /**
38 * Base class for form elements of FormEngine. Contains several helper methods used by single elements.
39 */
40 abstract class AbstractFormElement extends AbstractNode
41 {
42 /**
43 * Default width value for a couple of elements like text
44 *
45 * @var int
46 */
47 protected $defaultInputWidth = 30;
48
49 /**
50 * Minimum width value for a couple of elements like text
51 *
52 * @var int
53 */
54 protected $minimumInputWidth = 10;
55
56 /**
57 * Maximum width value for a couple of elements like text
58 *
59 * @var int
60 */
61 protected $maxInputWidth = 50;
62
63 /**
64 * @var \TYPO3\CMS\Backend\Clipboard\Clipboard|NULL
65 */
66 protected $clipboard = null;
67
68 /**
69 * @var NodeFactory
70 */
71 protected $nodeFactory;
72
73 /**
74 * Container objects give $nodeFactory down to other containers.
75 *
76 * @param NodeFactory $nodeFactory
77 * @param array $data
78 */
79 public function __construct(NodeFactory $nodeFactory, array $data)
80 {
81 parent::__construct($nodeFactory, $data);
82 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
83 // @todo: this must vanish as soon as elements are clean
84 $this->nodeFactory = $nodeFactory;
85 }
86
87 /**
88 * @return bool TRUE if wizards are disabled on a global level
89 */
90 protected function isWizardsDisabled()
91 {
92 return !empty($this->data['disabledWizards']);
93 }
94
95 /**
96 * Returns the max width in pixels for an elements like input and text
97 *
98 * @param int $size The abstract size value (1-48)
99 * @return int Maximum width in pixels
100 */
101 protected function formMaxWidth($size = 48)
102 {
103 $compensationForLargeDocuments = 1.33;
104 $compensationForFormFields = 12;
105
106 $size = round($size * $compensationForLargeDocuments);
107 return ceil($size * $compensationForFormFields);
108 }
109
110 /**
111 * @var IconFactory
112 */
113 protected $iconFactory;
114
115 /**
116 * Rendering wizards for form fields.
117 *
118 * @param array $itemKinds Array with the real item in the first value
119 * @param array $wizConf The "wizards" key from the config array for the field (from TCA)
120 * @param string $table Table name
121 * @param array $row The record array
122 * @param string $field The field name
123 * @param array $PA Additional configuration array.
124 * @param string $itemName The field name
125 * @param array $specConf Special configuration if available.
126 * @param bool $RTE Whether the RTE could have been loaded.
127 *
128 * @return string The new item value.
129 * @throws \InvalidArgumentException
130 */
131 protected function renderWizards($itemKinds, $wizConf, $table, $row, $field, $PA, $itemName, $specConf, $RTE = false)
132 {
133 // Return not changed main item directly if wizards are disabled
134 if (!is_array($wizConf) || $this->isWizardsDisabled()) {
135 return $itemKinds[0];
136 }
137
138 $languageService = $this->getLanguageService();
139
140 $fieldChangeFunc = $PA['fieldChangeFunc'];
141 $item = $itemKinds[0];
142 $md5ID = 'ID' . GeneralUtility::shortMD5($itemName);
143 $prefixOfFormElName = 'data[' . $table . '][' . $row['uid'] . '][' . $field . ']';
144 $flexFormPath = '';
145 if (GeneralUtility::isFirstPartOfStr($PA['itemFormElName'], $prefixOfFormElName)) {
146 $flexFormPath = str_replace('][', '/', substr($PA['itemFormElName'], strlen($prefixOfFormElName) + 1, -1));
147 }
148
149 // Add a suffix-value if the item is a selector box with renderType "selectSingleBox":
150 if ($PA['fieldConf']['config']['type'] === 'select' && (int)$PA['fieldConf']['config']['maxitems'] > 1 && $PA['fieldConf']['config']['renderType'] === 'selectSingleBox') {
151 $itemName .= '[]';
152 }
153
154 // Contains wizard identifiers enabled for this record type, see "special configuration" docs
155 $wizardsEnabledByType = $specConf['wizards']['parameters'];
156
157 $buttonWizards = [];
158 $otherWizards = [];
159 foreach ($wizConf as $wizardIdentifier => $wizardConfiguration) {
160 if (!isset($wizardConfiguration['module']['name']) && isset($wizardConfiguration['script'])) {
161 throw new \InvalidArgumentException('The way registering a wizard in TCA has changed in 6.2 and was removed in CMS 7. '
162 . 'Please set module[name]=module_name instead of using script=path/to/script.php in your TCA. ', 1437750231);
163 }
164
165 // If an identifier starts with "_", this is a configuration option like _POSITION and not a wizard
166 if ($wizardIdentifier[0] === '_') {
167 continue;
168 }
169
170 // Sanitize wizard type
171 $wizardConfiguration['type'] = (string)$wizardConfiguration['type'];
172
173 // Wizards can be shown based on selected "type" of record. If this is the case, the wizard configuration
174 // is set to enableByTypeConfig = 1, and the wizardIdentifier is found in $wizardsEnabledByType
175 $wizardIsEnabled = true;
176 if (
177 isset($wizardConfiguration['enableByTypeConfig'])
178 && (bool)$wizardConfiguration['enableByTypeConfig']
179 && (!is_array($wizardsEnabledByType) || !in_array($wizardIdentifier, $wizardsEnabledByType))
180 ) {
181 $wizardIsEnabled = false;
182 }
183 // Disable if wizard is for RTE fields only and the handled field is no RTE field or RTE can not be loaded
184 if (isset($wizardConfiguration['RTEonly']) && (bool)$wizardConfiguration['RTEonly'] && !$RTE) {
185 $wizardIsEnabled = false;
186 }
187 // Disable if wizard is for not-new records only and we're handling a new record
188 if (isset($wizardConfiguration['notNewRecords']) && $wizardConfiguration['notNewRecords'] && !MathUtility::canBeInterpretedAsInteger($row['uid'])) {
189 $wizardIsEnabled = false;
190 }
191 // Wizard types script, colorbox and popup must contain a module name configuration
192 if (!isset($wizardConfiguration['module']['name']) && in_array($wizardConfiguration['type'], ['script', 'colorbox', 'popup'], true)) {
193 $wizardIsEnabled = false;
194 }
195
196 if (!$wizardIsEnabled) {
197 continue;
198 }
199
200 // Title / icon:
201 $iTitle = htmlspecialchars($languageService->sL($wizardConfiguration['title']));
202 if (isset($wizardConfiguration['icon'])) {
203 $icon = FormEngineUtility::getIconHtml($wizardConfiguration['icon'], $iTitle, $iTitle);
204 } else {
205 $icon = $iTitle;
206 }
207
208 switch ($wizardConfiguration['type']) {
209 case 'userFunc':
210 $params = [];
211 $params['params'] = $wizardConfiguration['params'];
212 $params['exampleImg'] = $wizardConfiguration['exampleImg'];
213 $params['table'] = $table;
214 $params['uid'] = $row['uid'];
215 $params['pid'] = $row['pid'];
216 $params['field'] = $field;
217 $params['flexFormPath'] = $flexFormPath;
218 $params['md5ID'] = $md5ID;
219 $params['returnUrl'] = $this->data['returnUrl'];
220
221 $params['formName'] = 'editform';
222 $params['itemName'] = $itemName;
223 $params['hmac'] = GeneralUtility::hmac($params['formName'] . $params['itemName'], 'wizard_js');
224 $params['fieldChangeFunc'] = $fieldChangeFunc;
225 $params['fieldChangeFuncHash'] = GeneralUtility::hmac(serialize($fieldChangeFunc));
226
227 $params['item'] = &$item;
228 $params['icon'] = $icon;
229 $params['iTitle'] = $iTitle;
230 $params['wConf'] = $wizardConfiguration;
231 $params['row'] = $row;
232 $otherWizards[] = GeneralUtility::callUserFunction($wizardConfiguration['userFunc'], $params, $this);
233 break;
234
235 case 'script':
236 $params = [];
237 $params['params'] = $wizardConfiguration['params'];
238 $params['exampleImg'] = $wizardConfiguration['exampleImg'];
239 $params['table'] = $table;
240 $params['uid'] = $row['uid'];
241 $params['pid'] = $row['pid'];
242 $params['field'] = $field;
243 $params['flexFormPath'] = $flexFormPath;
244 $params['md5ID'] = $md5ID;
245 $params['returnUrl'] = $this->data['returnUrl'];
246
247 // Resolving script filename and setting URL.
248 $urlParameters = [];
249 if (isset($wizardConfiguration['module']['urlParameters']) && is_array($wizardConfiguration['module']['urlParameters'])) {
250 $urlParameters = $wizardConfiguration['module']['urlParameters'];
251 }
252 $wScript = BackendUtility::getModuleUrl($wizardConfiguration['module']['name'], $urlParameters, '');
253 $url = $wScript . (strstr($wScript, '?') ? '' : '?') . GeneralUtility::implodeArrayForUrl('', ['P' => $params]);
254 $buttonWizards[] =
255 '<a class="btn btn-default" href="' . htmlspecialchars($url) . '" onclick="this.blur(); return !TBE_EDITOR.isFormChanged();">'
256 . $icon .
257 '</a>';
258 break;
259
260 case 'popup':
261 $params = [];
262 $params['params'] = $wizardConfiguration['params'];
263 $params['exampleImg'] = $wizardConfiguration['exampleImg'];
264 $params['table'] = $table;
265 $params['uid'] = $row['uid'];
266 $params['pid'] = $row['pid'];
267 $params['field'] = $field;
268 $params['flexFormPath'] = $flexFormPath;
269 $params['md5ID'] = $md5ID;
270 $params['returnUrl'] = $this->data['returnUrl'];
271
272 $params['formName'] = 'editform';
273 $params['itemName'] = $itemName;
274 $params['hmac'] = GeneralUtility::hmac($params['formName'] . $params['itemName'], 'wizard_js');
275 $params['fieldChangeFunc'] = $fieldChangeFunc;
276 $params['fieldChangeFuncHash'] = GeneralUtility::hmac(serialize($fieldChangeFunc));
277
278 // Resolving script filename and setting URL.
279 $urlParameters = [];
280 if (isset($wizardConfiguration['module']['urlParameters']) && is_array($wizardConfiguration['module']['urlParameters'])) {
281 $urlParameters = $wizardConfiguration['module']['urlParameters'];
282 }
283 $wScript = BackendUtility::getModuleUrl($wizardConfiguration['module']['name'], $urlParameters, '');
284 $url = $wScript . (strstr($wScript, '?') ? '' : '?') . GeneralUtility::implodeArrayForUrl('', ['P' => $params]);
285
286 $onlyIfSelectedJS = '';
287 if (isset($wizardConfiguration['popup_onlyOpenIfSelected']) && $wizardConfiguration['popup_onlyOpenIfSelected']) {
288 $notSelectedText = $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:mess.noSelItemForEdit');
289 $onlyIfSelectedJS =
290 'if (!TBE_EDITOR.curSelected(' . GeneralUtility::quoteJSvalue($itemName) . ')){' .
291 'alert(' . GeneralUtility::quoteJSvalue($notSelectedText) . ');' .
292 'return false;' .
293 '}';
294 }
295 $aOnClick =
296 'this.blur();' .
297 $onlyIfSelectedJS .
298 'vHWin=window.open(' . GeneralUtility::quoteJSvalue($url) . '+\'&P[currentValue]=\'+TBE_EDITOR.rawurlencode(' .
299 'document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value,300' .
300 ')' .
301 '+\'&P[currentSelectedValues]=\'+TBE_EDITOR.curSelected(' . GeneralUtility::quoteJSvalue($itemName) . '),' .
302 GeneralUtility::quoteJSvalue('popUp' . $md5ID) . ',' .
303 GeneralUtility::quoteJSvalue($wizardConfiguration['JSopenParams']) .
304 ');' .
305 'vHWin.focus();' .
306 'return false;';
307
308 $buttonWizards[] =
309 '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($aOnClick) . '">' .
310 $icon .
311 '</a>';
312 break;
313
314 case 'slider':
315 $params = [];
316 $params['fieldConfig'] = $PA['fieldConf']['config'];
317 $params['field'] = $field;
318 $params['table'] = $table;
319 $params['flexFormPath'] = $flexFormPath;
320 $params['md5ID'] = $md5ID;
321 $params['itemName'] = $itemName;
322 $params['wConf'] = $wizardConfiguration;
323 $params['row'] = $row;
324
325 /** @var ValueSliderWizard $wizard */
326 $wizard = GeneralUtility::makeInstance(ValueSliderWizard::class);
327 $otherWizards[] = $wizard->renderWizard($params);
328 break;
329
330 case 'select':
331 // The select wizard is a select drop down added to the main element. It provides all the functionality
332 // that select items can do for us, so we process this element via data processing.
333 // @todo: This should be embedded in an own provider called in the main data group to not handle this on the fly here
334
335 // Select wizard page TS can be set in TCEFORM."table"."field".wizards."wizardName"
336 $pageTsConfig = [];
337 if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$field . '.']['wizards.'][$wizardIdentifier . '.'])
338 && is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$field . '.']['wizards.'][$wizardIdentifier . '.'])
339 ) {
340 $pageTsConfig['TCEFORM.']['dummySelectWizard.'][$wizardIdentifier . '.'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$field . '.']['wizards.'][$wizardIdentifier . '.'];
341 }
342 $selectWizardDataInput = [
343 'tableName' => 'dummySelectWizard',
344 'command' => 'edit',
345 'pageTsConfig' => $pageTsConfig,
346 'processedTca' => [
347 'ctrl' => [],
348 'columns' => [
349 $wizardIdentifier => [
350 'type' => 'select',
351 'renderType' => 'selectSingle',
352 'config' => $wizardConfiguration,
353 ],
354 ],
355 ],
356 ];
357 /** @var OnTheFly $formDataGroup */
358 $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
359 $formDataGroup->setProviderList([ TcaSelectItems::class ]);
360 /** @var FormDataCompiler $formDataCompiler */
361 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
362 $compilerResult = $formDataCompiler->compile($selectWizardDataInput);
363 $selectWizardItems = $compilerResult['processedTca']['columns'][$wizardIdentifier]['config']['items'];
364
365 $options = [];
366 $options[] = '<option>' . $iTitle . '</option>';
367 foreach ($selectWizardItems as $selectWizardItem) {
368 $options[] = '<option value="' . htmlspecialchars($selectWizardItem[1]) . '">' . htmlspecialchars($selectWizardItem[0]) . '</option>';
369 }
370 if ($wizardConfiguration['mode'] == 'append') {
371 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0].value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
372 } elseif ($wizardConfiguration['mode'] == 'prepend') {
373 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0].value+=\'\'+this.options[this.selectedIndex].value';
374 } else {
375 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0].value=this.options[this.selectedIndex].value';
376 }
377 $otherWizards[] =
378 '<select' .
379 ' id="' . StringUtility::getUniqueId('tceforms-select-') . '"' .
380 ' class="form-control tceforms-select tceforms-wizardselect"' .
381 ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"' .
382 '>' .
383 implode('', $options) .
384 '</select>';
385 break;
386 case 'suggest':
387 if (!empty($PA['fieldTSConfig']['suggest.']['default.']['hide'])) {
388 break;
389 }
390 // The suggest wizard needs to know if we're in flex form scope to use the dataStructureIdentifier.
391 // If so, add the processedTca of the flex config as wizard argument.
392 $flexFormConfig = [];
393 if ($this->data['processedTca']['columns'][$field]['config']['type'] === 'flex') {
394 $flexFormConfig = $this->data['processedTca']['columns'][$field];
395 }
396 /** @var SuggestWizard $suggestWizard */
397 $suggestWizard = GeneralUtility::makeInstance(SuggestWizard::class);
398 $otherWizards[] = $suggestWizard->renderSuggestSelector($PA['itemFormElName'], $table, $field, $row, $PA, $flexFormConfig);
399 break;
400 }
401 }
402
403 // For each rendered wizard, put them together around the item.
404 if (!empty($buttonWizards) || !empty($otherWizards)) {
405 $innerContent = '';
406 if (!empty($buttonWizards)) {
407 $innerContent .= '<div class="btn-group' . ($wizConf['_VERTICAL'] ? ' btn-group-vertical' : '') . '">' . implode('', $buttonWizards) . '</div>';
408 }
409 $innerContent .= implode(' ', $otherWizards);
410
411 // Position
412 $classes = ['form-wizards-wrap'];
413 if ($wizConf['_POSITION'] === 'left') {
414 $classes[] = 'form-wizards-aside';
415 $innerContent = '<div class="form-wizards-items">' . $innerContent . '</div><div class="form-wizards-element">' . $item . '</div>';
416 } elseif ($wizConf['_POSITION'] === 'top') {
417 $classes[] = 'form-wizards-top';
418 $innerContent = '<div class="form-wizards-items">' . $innerContent . '</div><div class="form-wizards-element">' . $item . '</div>';
419 } elseif ($wizConf['_POSITION'] === 'bottom') {
420 $classes[] = 'form-wizards-bottom';
421 $innerContent = '<div class="form-wizards-element">' . $item . '</div><div class="form-wizards-items">' . $innerContent . '</div>';
422 } else {
423 $classes[] = 'form-wizards-aside';
424 $innerContent = '<div class="form-wizards-element">' . $item . '</div><div class="form-wizards-items">' . $innerContent . '</div>';
425 }
426 $item = '
427 <div class="' . implode(' ', $classes) . '">
428 ' . $innerContent . '
429 </div>';
430 }
431
432 return $item;
433 }
434
435 /**
436 * Prints the selector box form-field for the db/file/select elements (multiple)
437 *
438 * @param string $fName Form element name
439 * @param string $mode Mode "db", "file" (internal_type for the "group" type) OR blank (then for the "select" type)
440 * @param string $allowed Commalist of "allowed
441 * @param array $itemArray The array of items. For "select" and "group"/"file" this is just a set of value. For "db" its an array of arrays with table/uid pairs.
442 * @param string $selector Alternative selector box.
443 * @param array $params An array of additional parameters, eg: "size", "info", "headers" (array with "selector" and "items"), "noBrowser", "thumbnails
444 * @param null $_ unused (onFocus in the past), will be removed in TYPO3 CMS 9
445 * @param string $table (optional) Table name processing for
446 * @param string $field (optional) Field of table name processing for
447 * @param string $uid (optional) uid of table record processing for
448 * @param array $config (optional) The TCA field config
449 * @return string The form fields for the selection.
450 * @throws \UnexpectedValueException
451 * @todo: Hack this mess into pieces and inline to group / select element depending on what they need
452 */
453 protected function dbFileIcons($fName, $mode, $allowed, $itemArray, $selector = '', $params = [], $_ = null, $table = '', $field = '', $uid = '', $config = [])
454 {
455 $languageService = $this->getLanguageService();
456 $disabled = '';
457 if ($params['readOnly']) {
458 $disabled = ' disabled="disabled"';
459 }
460 // INIT
461 $uidList = [];
462 $opt = [];
463 $itemArrayC = 0;
464 // Creating <option> elements:
465 if (is_array($itemArray)) {
466 $itemArrayC = count($itemArray);
467 switch ($mode) {
468 case 'db':
469 foreach ($itemArray as $pp) {
470 $pRec = BackendUtility::getRecordWSOL($pp['table'], $pp['id']);
471 if (is_array($pRec)) {
472 $pTitle = BackendUtility::getRecordTitle($pp['table'], $pRec, false, true);
473 $pUid = $pp['table'] . '_' . $pp['id'];
474 $uidList[] = $pUid;
475 $title = htmlspecialchars($pTitle);
476 $opt[] = '<option value="' . htmlspecialchars($pUid) . '" title="' . $title . '">' . $title . '</option>';
477 }
478 }
479 break;
480 case 'file_reference':
481
482 case 'file':
483 foreach ($itemArray as $item) {
484 $itemParts = explode('|', $item);
485 $uidList[] = ($pUid = ($pTitle = $itemParts[0]));
486 $title = htmlspecialchars(rawurldecode($itemParts[1]));
487 $opt[] = '<option value="' . htmlspecialchars(rawurldecode($itemParts[0])) . '" title="' . $title . '">' . $title . '</option>';
488 }
489 break;
490 case 'folder':
491 foreach ($itemArray as $pp) {
492 $pParts = explode('|', $pp);
493 $uidList[] = ($pUid = ($pTitle = $pParts[0]));
494 $title = htmlspecialchars(rawurldecode($pParts[0]));
495 $opt[] = '<option value="' . htmlspecialchars(rawurldecode($pParts[0])) . '" title="' . $title . '">' . $title . '</option>';
496 }
497 break;
498 default:
499 foreach ($itemArray as $pp) {
500 $pParts = explode('|', $pp, 2);
501 $uidList[] = ($pUid = $pParts[0]);
502 $pTitle = $pParts[1];
503 $title = htmlspecialchars(rawurldecode($pTitle));
504 $opt[] = '<option value="' . htmlspecialchars(rawurldecode($pUid)) . '" title="' . $title . '">' . $title . '</option>';
505 }
506 }
507 }
508 // Create selector box of the options
509 $sSize = $params['autoSizeMax']
510 ? MathUtility::forceIntegerInRange($itemArrayC + 1, MathUtility::forceIntegerInRange($params['size'], 1), $params['autoSizeMax'])
511 : $params['size'];
512 if (!$selector) {
513 $maxItems = (int)($params['maxitems'] ?? 0);
514 $size = (int)($params['size'] ?? 0);
515 $classes = ['form-control', 'tceforms-multiselect'];
516 if ($maxItems === 1) {
517 $classes[] = 'form-select-no-siblings';
518 }
519 $isMultiple = $maxItems !== 1 && $size !== 1;
520 $selector = '<select id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '" '
521 . ($params['noList'] ? 'style="display: none"' : 'size="' . $sSize . '" class="' . implode(' ', $classes) . '"')
522 . ($isMultiple ? ' multiple="multiple"' : '')
523 . ' data-formengine-input-name="' . htmlspecialchars($fName) . '" ' . $this->getValidationDataAsDataAttribute($config) . $params['style'] . $disabled . '>' . implode('', $opt)
524 . '</select>';
525 }
526 $icons = [
527 'L' => [],
528 'R' => []
529 ];
530 $rOnClickInline = '';
531 if (!$params['readOnly'] && !$params['noList']) {
532 if (!$params['noBrowser']) {
533 // Check against inline uniqueness
534 /** @var InlineStackProcessor $inlineStackProcessor */
535 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
536 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
537 $aOnClickInline = '';
538 if ($this->data['isInlineChild'] && $this->data['inlineParentUid']) {
539 if ($this->data['inlineParentConfig']['foreign_table'] === $table
540 && $this->data['inlineParentConfig']['foreign_unique'] === $field
541 ) {
542 $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']) . '-' . $table;
543 $aOnClickInline = $objectPrefix . '|inline.checkUniqueElement|inline.setUniqueElement';
544 $rOnClickInline = 'inline.revertUnique(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',null,' . GeneralUtility::quoteJSvalue($uid) . ');';
545 }
546 }
547 if (is_array($config['appearance']) && isset($config['appearance']['elementBrowserType'])) {
548 $elementBrowserType = $config['appearance']['elementBrowserType'];
549 } else {
550 $elementBrowserType = $mode;
551 }
552 if (is_array($config['appearance']) && isset($config['appearance']['elementBrowserAllowed'])) {
553 $elementBrowserAllowed = $config['appearance']['elementBrowserAllowed'];
554 } else {
555 $elementBrowserAllowed = $allowed;
556 }
557 $aOnClick = 'setFormValueOpenBrowser(' . GeneralUtility::quoteJSvalue($elementBrowserType) . ','
558 . GeneralUtility::quoteJSvalue(($fName . '|||' . $elementBrowserAllowed . '|' . $aOnClickInline)) . '); return false;';
559 $icons['R'][] = '
560 <a href="#"
561 onclick="' . htmlspecialchars($aOnClick) . '"
562 class="btn btn-default"
563 title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.browse_' . ($mode == 'db' ? 'db' : 'file'))) . '">
564 ' . $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render() . '
565 </a>';
566 }
567 if (!$params['dontShowMoveIcons']) {
568 if ($sSize >= 5) {
569 $icons['L'][] = '
570 <a href="#"
571 class="btn btn-default t3js-btn-moveoption-top"
572 data-fieldname="' . $fName . '"
573 title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '">
574 ' . $this->iconFactory->getIcon('actions-move-to-top', Icon::SIZE_SMALL)->render() . '
575 </a>';
576 }
577 $icons['L'][] = '
578 <a href="#"
579 class="btn btn-default t3js-btn-moveoption-up"
580 data-fieldname="' . $fName . '"
581 title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '">
582 ' . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '
583 </a>';
584 $icons['L'][] = '
585 <a href="#"
586 class="btn btn-default t3js-btn-moveoption-down"
587 data-fieldname="' . $fName . '"
588 title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '">
589 ' . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '
590 </a>';
591 if ($sSize >= 5) {
592 $icons['L'][] = '
593 <a href="#"
594 class="btn btn-default t3js-btn-moveoption-bottom"
595 data-fieldname="' . $fName . '"
596 title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '">
597 ' . $this->iconFactory->getIcon('actions-move-to-bottom', Icon::SIZE_SMALL)->render() . '
598 </a>';
599 }
600 }
601 $clipElements = $this->getClipboardElements($allowed, $mode);
602 if (!empty($clipElements)) {
603 $aOnClick = '';
604 foreach ($clipElements as $elValue) {
605 if ($mode == 'db') {
606 list($itemTable, $itemUid) = explode('|', $elValue);
607 $recordTitle = BackendUtility::getRecordTitle($itemTable, BackendUtility::getRecordWSOL($itemTable, $itemUid));
608 $itemTitle = GeneralUtility::quoteJSvalue($recordTitle);
609 $elValue = $itemTable . '_' . $itemUid;
610 } else {
611 // 'file', 'file_reference' and 'folder' mode
612 $itemTitle = 'unescape(' . GeneralUtility::quoteJSvalue(rawurlencode(basename($elValue))) . ')';
613 }
614 $aOnClick .= 'setFormValueFromBrowseWin(' . GeneralUtility::quoteJSvalue($fName) . ',unescape('
615 . GeneralUtility::quoteJSvalue(rawurlencode(str_replace('%20', ' ', $elValue))) . '),' . $itemTitle . ',' . $itemTitle . ');';
616 }
617 $aOnClick .= 'return false;';
618 $icons['R'][] = '
619 <a href="#"
620 class="btn btn-default"
621 onclick="' . htmlspecialchars($aOnClick) . '"
622 title="' . htmlspecialchars(sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.clipInsert_' . ($mode == 'db' ? 'db' : 'file')), count($clipElements))) . '">
623 ' . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render() . '
624 </a>';
625 }
626 }
627 if (!$params['readOnly'] && !$params['noDelete']) {
628 $icons['L'][] = '
629 <a href="#"
630 class="btn btn-default t3js-btn-removeoption"
631 onClick="' . $rOnClickInline . '"
632 data-fieldname="' . $fName . '"
633 title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '">
634 ' . $this->iconFactory->getIcon('actions-selection-delete', Icon::SIZE_SMALL)->render() . '
635 </a>';
636 }
637
638 // Thumbnails
639 $imagesOnly = false;
640 if ($params['thumbnails'] && $params['allowed']) {
641 // In case we have thumbnails, check if only images are allowed.
642 // In this case, render them below the field, instead of to the right
643 $allowedExtensionList = $params['allowed'];
644 $imageExtensionList = GeneralUtility::trimExplode(',', strtolower($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']), true);
645 $imagesOnly = true;
646 foreach ($allowedExtensionList as $allowedExtension) {
647 if (!ArrayUtility::inArray($imageExtensionList, $allowedExtension)) {
648 $imagesOnly = false;
649 break;
650 }
651 }
652 }
653 $thumbnails = '';
654 if (is_array($params['thumbnails']) && !empty($params['thumbnails'])) {
655 if ($imagesOnly) {
656 $thumbnails .= '<ul class="list-inline">';
657 foreach ($params['thumbnails'] as $thumbnail) {
658 $thumbnails .= '<li><span class="thumbnail">' . $thumbnail['image'] . '</span></li>';
659 }
660 $thumbnails .= '</ul>';
661 } else {
662 $thumbnails .= '<div class="table-fit"><table class="table table-white"><tbody>';
663 foreach ($params['thumbnails'] as $thumbnail) {
664 $thumbnails .= '
665 <tr>
666 <td class="col-icon">
667 ' . ($config['internal_type'] === 'db'
668 ? BackendUtility::wrapClickMenuOnIcon($thumbnail['image'], $thumbnail['table'], $thumbnail['uid'], 1, '', '+copy,info,edit,view')
669 : $thumbnail['image']) . '
670 </td>
671 <td class="col-title">
672 ' . ($config['internal_type'] === 'db'
673 ? BackendUtility::wrapClickMenuOnIcon($thumbnail['name'], $thumbnail['table'], $thumbnail['uid'], 1, '', '+copy,info,edit,view')
674 : $thumbnail['name']) . '
675 ' . ($config['internal_type'] === 'db' ? ' <span class="text-muted">[' . $thumbnail['uid'] . ']</span>' : '') . '
676 </td>
677 </tr>
678 ';
679 }
680 $thumbnails .= '</tbody></table></div>';
681 }
682 }
683
684 // Allowed Tables
685 $allowedTables = '';
686 if (is_array($params['allowedTables']) && !empty($params['allowedTables']) && !$params['hideAllowedTables']) {
687 $allowedTables .= '<div class="help-block">';
688 foreach ($params['allowedTables'] as $key => $item) {
689 if (is_array($item)) {
690 if (empty($params['readOnly'])) {
691 $allowedTables .= '<a href="#" onClick="' . htmlspecialchars($item['onClick']) . '" class="btn btn-default">' . $item['icon'] . ' ' . htmlspecialchars($item['name']) . '</a> ';
692 } else {
693 $allowedTables .= '<span>' . htmlspecialchars($item['name']) . '</span> ';
694 }
695 } elseif ($key === 'name') {
696 $allowedTables .= '<span>' . htmlspecialchars($item) . '</span> ';
697 }
698 }
699 $allowedTables .= '</div>';
700 }
701 // Allowed
702 $allowedList = '';
703 if (is_array($params['allowed']) && !empty($params['allowed'])) {
704 foreach ($params['allowed'] as $item) {
705 $allowedList .= '<span class="label label-success">' . strtoupper($item) . '</span> ';
706 }
707 }
708 // Disallowed
709 $disallowedList = '';
710 if (is_array($params['disallowed']) && !empty($params['disallowed'])) {
711 foreach ($params['disallowed'] as $item) {
712 $disallowedList .= '<span class="label label-danger">' . strtoupper($item) . '</span> ';
713 }
714 }
715 // Rightbox
716 $rightbox = ($params['rightbox'] ?: '');
717
718 // Hook: dbFileIcons_postProcess (requested by FAL-team for use with the "fal" extension)
719 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms.php']['dbFileIcons'])) {
720 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms.php']['dbFileIcons'] as $classRef) {
721 $hookObject = GeneralUtility::getUserObj($classRef);
722 if (!$hookObject instanceof DatabaseFileIconsHookInterface) {
723 throw new \UnexpectedValueException($classRef . ' must implement interface ' . DatabaseFileIconsHookInterface::class, 1290167704);
724 }
725 $additionalParams = [
726 'mode' => $mode,
727 'allowed' => $allowed,
728 'itemArray' => $itemArray,
729 'table' => $table,
730 'field' => $field,
731 'uid' => $uid,
732 'config' => $GLOBALS['TCA'][$table]['columns'][$field]
733 ];
734 $hookObject->dbFileIcons_postProcess($params, $selector, $thumbnails, $icons, $rightbox, $fName, $uidList, $additionalParams, $this);
735 }
736 }
737
738 // Output
739 $str = '
740 ' . ($params['headers']['selector'] ? '<label>' . $params['headers']['selector'] . '</label>' : '') . '
741 <div class="form-wizards-wrap form-wizards-aside">
742 <div class="form-wizards-element">
743 ' . $selector . '
744 ' . (!$params['noList'] && !empty($allowedTables) ? $allowedTables : '') . '
745 ' . (!$params['noList'] && (!empty($allowedList) || !empty($disallowedList))
746 ? '<div class="help-block">' . $allowedList . $disallowedList . ' </div>'
747 : '') . '
748 </div>
749 ' . (!empty($icons['L']) ? '<div class="form-wizards-items"><div class="btn-group-vertical">' . implode('', $icons['L']) . '</div></div>' : '') . '
750 ' . (!empty($icons['R']) ? '<div class="form-wizards-items"><div class="btn-group-vertical">' . implode('', $icons['R']) . '</div></div>' : '') . '
751 </div>
752 ';
753 if ($rightbox) {
754 $str = '
755 <div class="form-multigroup-wrap t3js-formengine-field-group">
756 <div class="form-multigroup-item form-multigroup-element">' . $str . '</div>
757 <div class="form-multigroup-item form-multigroup-element">
758 ' . ($params['headers']['items'] ? '<label>' . $params['headers']['items'] . '</label>' : '') . '
759 ' . ($params['headers']['selectorbox'] ? '<div class="form-multigroup-item-wizard">' . $params['headers']['selectorbox'] . '</div>' : '') . '
760 ' . $rightbox . '
761 </div>
762 </div>
763 ';
764 }
765 $str .= $thumbnails;
766
767 // Creating the hidden field which contains the actual value as a comma list.
768 $str .= '<input type="hidden" name="' . $fName . '" value="' . htmlspecialchars(implode(',', $uidList)) . '" />';
769 return $str;
770 }
771
772 /**
773 * Returns array of elements from clipboard to insert into GROUP element box.
774 *
775 * @param string $allowed Allowed elements, Eg "pages,tt_content", "gif,jpg,jpeg,png
776 * @param string $mode Mode of relations: "db" or "file
777 * @return array Array of elements in values (keys are insignificant), if none found, empty array.
778 */
779 protected function getClipboardElements($allowed, $mode)
780 {
781 if (!is_object($this->clipboard)) {
782 $this->clipboard = GeneralUtility::makeInstance(Clipboard::class);
783 $this->clipboard->initializeClipboard();
784 }
785
786 $output = [];
787 switch ($mode) {
788 case 'file_reference':
789
790 case 'file':
791 $elFromTable = $this->clipboard->elFromTable('_FILE');
792 $allowedExts = GeneralUtility::trimExplode(',', $allowed, true);
793 // If there are a set of allowed extensions, filter the content:
794 if ($allowedExts) {
795 foreach ($elFromTable as $elValue) {
796 $pI = pathinfo($elValue);
797 $ext = strtolower($pI['extension']);
798 if (in_array($ext, $allowedExts)) {
799 $output[] = $elValue;
800 }
801 }
802 } else {
803 // If all is allowed, insert all: (This does NOT respect any disallowed extensions,
804 // but those will be filtered away by the backend DataHandler)
805 $output = $elFromTable;
806 }
807 break;
808 case 'db':
809 $allowedTables = GeneralUtility::trimExplode(',', $allowed, true);
810 // All tables allowed for relation:
811 if (trim($allowedTables[0]) === '*') {
812 $output = $this->clipboard->elFromTable('');
813 } else {
814 // Only some tables, filter them:
815 foreach ($allowedTables as $tablename) {
816 $elFromTable = $this->clipboard->elFromTable($tablename);
817 $output = array_merge($output, $elFromTable);
818 }
819 }
820 $output = array_keys($output);
821 break;
822 }
823
824 return $output;
825 }
826
827 /**
828 * @return LanguageService
829 */
830 protected function getLanguageService()
831 {
832 return $GLOBALS['LANG'];
833 }
834 }