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