8ce6b1022410f8a1ed4cd74931499707ade40c46
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Container / InlineControlContainer.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\Container;
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\Form\FormDataCompiler;
18 use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
19 use TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems;
20 use TYPO3\CMS\Backend\Form\NodeFactory;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
22 use TYPO3\CMS\Core\Imaging\Icon;
23 use TYPO3\CMS\Core\Imaging\IconFactory;
24 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
25 use TYPO3\CMS\Core\Resource\Folder;
26 use TYPO3\CMS\Core\Utility\MathUtility;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
29 use TYPO3\CMS\Lang\LanguageService;
30 use TYPO3\CMS\Core\Utility\ArrayUtility;
31 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
32 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
33
34 /**
35 * Inline element entry container.
36 *
37 * This container is the entry step to rendering an inline element. It is created by SingleFieldContainer.
38 *
39 * The code creates the main structure for the single inline elements, initializes
40 * the inlineData array, that is manipulated and also returned back in its manipulated state.
41 * The "control" stuff of inline elements is rendered here, for example the "create new" button.
42 *
43 * For each existing inline relation an InlineRecordContainer is called for further processing.
44 */
45 class InlineControlContainer extends AbstractContainer
46 {
47 /**
48 * Inline data array used in JS, returned as JSON object to frontend
49 *
50 * @var array
51 */
52 protected $inlineData = array();
53
54 /**
55 * @var InlineStackProcessor
56 */
57 protected $inlineStackProcessor;
58
59 /**
60 * @var IconFactory
61 */
62 protected $iconFactory;
63
64 /**
65 * @var string[]
66 */
67 protected $requireJsModules = [];
68
69 /**
70 * Container objects give $nodeFactory down to other containers.
71 *
72 * @param NodeFactory $nodeFactory
73 * @param array $data
74 */
75 public function __construct(NodeFactory $nodeFactory, array $data)
76 {
77 parent::__construct($nodeFactory, $data);
78 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
79 }
80
81 /**
82 * Entry method
83 *
84 * @return array As defined in initializeResultArray() of AbstractNode
85 */
86 public function render()
87 {
88 $languageService = $this->getLanguageService();
89
90 $this->inlineData = $this->data['inlineData'];
91
92 /** @var InlineStackProcessor $inlineStackProcessor */
93 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
94 $this->inlineStackProcessor = $inlineStackProcessor;
95 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
96
97 $table = $this->data['tableName'];
98 $row = $this->data['databaseRow'];
99 $field = $this->data['fieldName'];
100 $parameterArray = $this->data['parameterArray'];
101
102 $resultArray = $this->initializeResultArray();
103
104 $config = $parameterArray['fieldConf']['config'];
105 $foreign_table = $config['foreign_table'];
106
107 $language = 0;
108 if (BackendUtility::isTableLocalizable($table)) {
109 $language = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
110 }
111
112 // Add the current inline job to the structure stack
113 $newStructureItem = array(
114 'table' => $table,
115 'uid' => $row['uid'],
116 'field' => $field,
117 'config' => $config,
118 'localizationMode' => BackendUtility::getInlineLocalizationMode($table, $config),
119 );
120 // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
121 if (!empty($parameterArray['itemFormElName'])) {
122 $flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']);
123 if ($flexFormParts !== null) {
124 $newStructureItem['flexform'] = $flexFormParts;
125 }
126 }
127 $inlineStackProcessor->pushStableStructureItem($newStructureItem);
128
129 // e.g. data[<table>][<uid>][<field>]
130 $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix();
131 // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
132 $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
133
134 $config['inline']['first'] = false;
135 // @todo: This initialization shouldn't be required data provider should take care this is set?
136 if (!is_array($this->data['parameterArray']['fieldConf']['children'])) {
137 $this->data['parameterArray']['fieldConf']['children'] = array();
138 }
139 $firstChild = reset($this->data['parameterArray']['fieldConf']['children']);
140 if (isset($firstChild['databaseRow']['uid'])) {
141 $config['inline']['first'] = $firstChild['databaseRow']['uid'];
142 }
143 $config['inline']['last'] = false;
144 $lastChild = end($this->data['parameterArray']['fieldConf']['children']);
145 if (isset($lastChild['databaseRow']['uid'])) {
146 $config['inline']['last'] = $lastChild['databaseRow']['uid'];
147 }
148
149 $top = $inlineStackProcessor->getStructureLevel(0);
150
151 $this->inlineData['config'][$nameObject] = array(
152 'table' => $foreign_table,
153 'md5' => md5($nameObject)
154 );
155 $this->inlineData['config'][$nameObject . '-' . $foreign_table] = array(
156 'min' => $config['minitems'],
157 'max' => $config['maxitems'],
158 'sortable' => $config['appearance']['useSortable'],
159 'top' => array(
160 'table' => $top['table'],
161 'uid' => $top['uid']
162 ),
163 'context' => array(
164 'config' => $config,
165 'hmac' => GeneralUtility::hmac(serialize($config)),
166 ),
167 );
168 $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
169
170 // If relations are required to be unique, get the uids that have already been used on the foreign side of the relation
171 $uniqueMax = 0;
172 $possibleRecords = [];
173 $uniqueIds = [];
174 if ($config['foreign_unique']) {
175 // If uniqueness *and* selector are set, they should point to the same field - so, get the configuration of one:
176 $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $config['foreign_unique']);
177 // Get the used unique ids:
178 $uniqueIds = $this->getUniqueIds($this->data['parameterArray']['fieldConf']['children'], $config, $selConfig['type'] == 'groupdb');
179 $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config, 'foreign_unique');
180 $uniqueMax = $config['appearance']['useCombination'] || $possibleRecords === false ? -1 : count($possibleRecords);
181 $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = array(
182 'max' => $uniqueMax,
183 'used' => $uniqueIds,
184 'type' => $selConfig['type'],
185 'table' => $config['foreign_table'],
186 'elTable' => $selConfig['table'],
187 // element/record table (one step down in hierarchy)
188 'field' => $config['foreign_unique'],
189 'selector' => $selConfig['selector'],
190 'possible' => $this->getPossibleRecordsFlat($possibleRecords)
191 );
192 }
193
194 $resultArray['inlineData'] = $this->inlineData;
195
196 // Render the localization links
197 $localizationLinks = '';
198 if ($language > 0 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0 && MathUtility::canBeInterpretedAsInteger($row['uid'])) {
199 // Add the "Localize all records" link before all child records:
200 if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) {
201 $localizationLinks .= ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config);
202 }
203 // Add the "Synchronize with default language" link before all child records:
204 if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) {
205 $localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config);
206 }
207 }
208
209 $numberOfFullChildren = 0;
210 foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
211 if (!$child['inlineIsDefaultLanguage']) {
212 $numberOfFullChildren ++;
213 }
214 }
215 // Define how to show the "Create new record" link - if there are more than maxitems, hide it
216 if ($numberOfFullChildren >= $config['maxitems'] || $uniqueMax > 0 && $numberOfFullChildren >= $uniqueMax) {
217 $config['inline']['inlineNewButtonStyle'] = 'display: none;';
218 $config['inline']['inlineNewRelationButtonStyle'] = 'display: none;';
219 }
220
221 // Render the level links (create new record):
222 $levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config);
223
224 // Wrap all inline fields of a record with a <div> (like a container)
225 $html = '<div class="form-group" id="' . $nameObject . '">';
226 // Add the level links before all child records:
227 if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
228 $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>';
229 }
230
231 // If it's required to select from possible child records (reusable children), add a selector box
232 if ($config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== false) {
233 // If not already set by the foreign_unique, set the possibleRecords here and the uniqueIds to an empty array
234 if (!$config['foreign_unique']) {
235 $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config);
236 $uniqueIds = array();
237 }
238 $selectorBox = $this->renderPossibleRecordsSelector($possibleRecords, $config, $uniqueIds);
239 $html .= $selectorBox . $localizationLinks;
240 }
241
242 $title = $languageService->sL($parameterArray['fieldConf']['label']);
243 $html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">';
244
245 $sortableRecordUids = [];
246 foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) {
247 $options['inlineParentUid'] = $row['uid'];
248 // @todo: this can be removed if this container no longer sets additional info to $config
249 $options['inlineParentConfig'] = $config;
250 $options['inlineData'] = $this->inlineData;
251 $options['inlineStructure'] = $inlineStackProcessor->getStructure();
252 $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
253 $options['renderType'] = 'inlineRecordContainer';
254 $childResult = $this->nodeFactory->create($options)->render();
255 $html .= $childResult['html'];
256 $childArray['html'] = '';
257 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult);
258 if (!$options['inlineIsDefaultLanguage']) {
259 // Don't add record to list of "valid" uids if it is only the default
260 // language record of a not yet localized child
261 $sortableRecordUids[] = $options['databaseRow']['uid'];
262 }
263 }
264
265 $html .= '</div>';
266
267 // Add the level links after all child records:
268 if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom') {
269 $html .= $levelLinks . $localizationLinks;
270 }
271 if (is_array($config['customControls'])) {
272 $html .= '<div id="' . $nameObject . '_customControls">';
273 foreach ($config['customControls'] as $customControlConfig) {
274 $parameters = array(
275 'table' => $table,
276 'field' => $field,
277 'row' => $row,
278 'nameObject' => $nameObject,
279 'nameForm' => $nameForm,
280 'config' => $config
281 );
282 $html .= GeneralUtility::callUserFunction($customControlConfig, $parameters, $this);
283 }
284 $html .= '</div>';
285 }
286 // Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post
287 if (count($sortableRecordUids) > 1 && $config['appearance']['useSortable']) {
288 $resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");';
289 }
290 $resultArray['requireJsModules'] = $this->requireJsModules;
291
292 // Publish the uids of the child records in the given order to the browser
293 $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
294 . $this->getValidationDataAsDataAttribute(array('type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']))
295 . ' class="inlineRecord" />';
296 // Close the wrap for all inline fields (container)
297 $html .= '</div>';
298
299 $resultArray['html'] = $html;
300 return $resultArray;
301 }
302
303 /**
304 * Gets the uids of a select/selector that should be unique and have already been used.
305 *
306 * @param array $children All inline records on this level
307 * @param array $conf The TCA field configuration of the inline field to be rendered
308 * @param bool $splitValue For usage with group/db, values come like "tx_table_123|Title%20abc", but we need "tx_table" and "123
309 * @return array The uids, that have been used already and should be used unique
310 */
311 protected function getUniqueIds($children, $conf = array(), $splitValue = false)
312 {
313 $uniqueIds = array();
314 if (isset($conf['foreign_unique']) && $conf['foreign_unique'] && !empty($children)) {
315 foreach ($children as $child) {
316 // Skip virtual records (e.g. shown in localization mode):
317 if (!$child['inlineIsDefaultLanguage']) {
318 $value = $child[$conf['foreign_unique']];
319 if (is_array($value)) {
320 $value = $value['0'];
321 }
322 // Split the value and extract the table and uid:
323 if ($splitValue) {
324 $valueParts = GeneralUtility::trimExplode('|', $value);
325 $itemParts = explode('_', $valueParts[0]);
326 $value = array(
327 'uid' => array_pop($itemParts),
328 'table' => implode('_', $itemParts)
329 );
330 }
331 $uniqueIds[$child['uid']] = $value;
332 }
333 }
334 }
335 return $uniqueIds;
336 }
337
338 /**
339 * Get possible records.
340 * Copied from FormEngine and modified.
341 *
342 * @param string $table The table name of the record
343 * @param string $field The field name which this element is supposed to edit
344 * @param array $row The record data array where the value(s) for the field can be found
345 * @param array $conf An array with additional configuration options.
346 * @param string $checkForConfField For which field in the foreign_table the possible records should be fetched
347 * @return mixed Array of possible record items; FALSE if type is "group/db", then everything could be "possible
348 */
349 protected function getPossibleRecords($table, $field, $row, $conf, $checkForConfField = 'foreign_selector')
350 {
351 // Field configuration from TCA:
352 $foreign_check = $conf[$checkForConfField];
353 $foreignConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($conf, $foreign_check);
354 $PA = $foreignConfig['PA'];
355 if ($foreignConfig['type'] == 'select') {
356 $pageTsConfig['TCEFORM.']['dummyTable.']['dummyField.'] = $PA['fieldTSConfig'];
357 $selectDataInput = [
358 'tableName' => 'dummyTable',
359 'command' => 'edit',
360 'pageTsConfig' => $pageTsConfig,
361 'processedTca' => [
362 'ctrl' => [],
363 'columns' => [
364 'dummyField' => $PA['fieldConf'],
365 ],
366 ],
367 ];
368
369 /** @var OnTheFly $formDataGroup */
370 $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
371 $formDataGroup->setProviderList([ TcaSelectItems::class ]);
372 /** @var FormDataCompiler $formDataCompiler */
373 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
374 $compilerResult = $formDataCompiler->compile($selectDataInput);
375 $selItems = $compilerResult['processedTca']['columns']['dummyField']['config']['items'];
376 } else {
377 $selItems = false;
378 }
379 return $selItems;
380 }
381
382 /**
383 * Makes a flat array from the $possibleRecords array.
384 * The key of the flat array is the value of the record,
385 * the value of the flat array is the label of the record.
386 *
387 * @param array $possibleRecords The possibleRecords array (for select fields)
388 * @return mixed A flat array with key=uid, value=label; if $possibleRecords isn't an array, FALSE is returned.
389 */
390 protected function getPossibleRecordsFlat($possibleRecords)
391 {
392 $flat = false;
393 if (is_array($possibleRecords)) {
394 $flat = array();
395 foreach ($possibleRecords as $record) {
396 $flat[$record[1]] = $record[0];
397 }
398 }
399 return $flat;
400 }
401
402 /**
403 * Creates the HTML code of a general link to be used on a level of inline children.
404 * The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'.
405 *
406 * @param string $type The link type, values are 'newRecord', 'localize' and 'synchronize'.
407 * @param string $objectPrefix The "path" to the child record to create (e.g. 'data-parentPageId-partenTable-parentUid-parentField-childTable]')
408 * @param array $conf TCA configuration of the parent(!) field
409 * @return string The HTML code of the new link, wrapped in a div
410 */
411 protected function getLevelInteractionLink($type, $objectPrefix, $conf = array())
412 {
413 $languageService = $this->getLanguageService();
414 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
415 $attributes = array();
416 switch ($type) {
417 case 'newRecord':
418 $title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createnew', true);
419 $icon = 'actions-document-new';
420 $className = 'typo3-newRecordLink';
421 $attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
422 $attributes['onclick'] = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ')';
423 if (!empty($conf['inline']['inlineNewButtonStyle'])) {
424 $attributes['style'] = $conf['inline']['inlineNewButtonStyle'];
425 }
426 if (!empty($conf['appearance']['newRecordLinkAddTitle'])) {
427 $title = sprintf(
428 $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createnew.link', true),
429 $languageService->sL($GLOBALS['TCA'][$conf['foreign_table']]['ctrl']['title'], true)
430 );
431 } elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') {
432 $title = $languageService->sL($conf['appearance']['newRecordLinkTitle'], true);
433 }
434 break;
435 case 'localize':
436 $title = $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localizeAllRecords', true);
437 $icon = 'actions-document-localize';
438 $className = 'typo3-localizationLink';
439 $attributes['class'] = 'btn btn-default';
440 $attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'localize\')';
441 break;
442 case 'synchronize':
443 $title = $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:synchronizeWithOriginalLanguage', true);
444 $icon = 'actions-document-synchronize';
445 $className = 'typo3-synchronizationLink';
446 $attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
447 $attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'synchronize\')';
448 break;
449 default:
450 $title = '';
451 $icon = '';
452 $className = '';
453 }
454 // Create the link:
455 $icon = $icon ? $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() : '';
456 $link = $this->wrapWithAnchor($icon . $title, '#', $attributes);
457 return '<div' . ($className ? ' class="' . $className . '"' : '') . 'title="' . $title . '">' . $link . '</div>';
458 }
459
460 /**
461 * Wraps a text with an anchor and returns the HTML representation.
462 *
463 * @param string $text The text to be wrapped by an anchor
464 * @param string $link The link to be used in the anchor
465 * @param array $attributes Array of attributes to be used in the anchor
466 * @return string The wrapped text as HTML representation
467 */
468 protected function wrapWithAnchor($text, $link, $attributes = array())
469 {
470 $link = trim($link);
471 $result = '<a href="' . ($link ?: '#') . '"';
472 foreach ($attributes as $key => $value) {
473 $result .= ' ' . $key . '="' . htmlspecialchars(trim($value)) . '"';
474 }
475 $result .= '>' . $text . '</a>';
476 return $result;
477 }
478
479 /**
480 * Get a selector as used for the select type, to select from all available
481 * records and to create a relation to the embedding record (e.g. like MM).
482 *
483 * @param array $selItems Array of all possible records
484 * @param array $conf TCA configuration of the parent(!) field
485 * @param array $uniqueIds The uids that have already been used and should be unique
486 * @return string A HTML <select> box with all possible records
487 */
488 protected function renderPossibleRecordsSelector($selItems, $conf, $uniqueIds = array())
489 {
490 $foreign_selector = $conf['foreign_selector'];
491 $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($conf, $foreign_selector);
492 $item = '';
493 if ($selConfig['type'] === 'select') {
494 $item = $this->renderPossibleRecordsSelectorTypeSelect($selItems, $conf, $selConfig['PA'], $uniqueIds);
495 } elseif ($selConfig['type'] === 'groupdb') {
496 $item = $this->renderPossibleRecordsSelectorTypeGroupDB($conf, $selConfig['PA']);
497 }
498 return $item;
499 }
500
501 /**
502 * Generate a link that opens an element browser in a new window.
503 * For group/db there is no way to use a "selector" like a <select>|</select>-box.
504 *
505 * @param array $conf TCA configuration of the parent(!) field
506 * @param array $PA An array with additional configuration options
507 * @return string A HTML link that opens an element browser in a new window
508 */
509 protected function renderPossibleRecordsSelectorTypeGroupDB($conf, &$PA)
510 {
511 $backendUser = $this->getBackendUserAuthentication();
512 $languageService = $this->getLanguageService();
513
514 $config = $PA['fieldConf']['config'];
515 ArrayUtility::mergeRecursiveWithOverrule($config, $conf);
516 $foreign_table = $config['foreign_table'];
517 $allowed = $config['allowed'];
518 $objectPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']) . '-' . $foreign_table;
519 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
520 $mode = 'db';
521 $showUpload = false;
522 if (!empty($config['appearance']['createNewRelationLinkTitle'])) {
523 $createNewRelationText = $languageService->sL($config['appearance']['createNewRelationLinkTitle'], true);
524 } else {
525 $createNewRelationText = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', true);
526 }
527 if (is_array($config['appearance'])) {
528 if (isset($config['appearance']['elementBrowserType'])) {
529 $mode = $config['appearance']['elementBrowserType'];
530 }
531 if ($mode === 'file') {
532 $showUpload = true;
533 }
534 if (isset($config['appearance']['fileUploadAllowed'])) {
535 $showUpload = (bool)$config['appearance']['fileUploadAllowed'];
536 }
537 if (isset($config['appearance']['elementBrowserAllowed'])) {
538 $allowed = $config['appearance']['elementBrowserAllowed'];
539 }
540 }
541 $browserParams = '|||' . $allowed . '|' . $objectPrefix . '|inline.checkUniqueElement||inline.importElement';
542 $onClick = 'setFormValueOpenBrowser(' . GeneralUtility::quoteJSvalue($mode) . ', ' . GeneralUtility::quoteJSvalue($browserParams) . '); return false;';
543
544 $buttonStyle = '';
545 if (isset($config['inline']['inlineNewRelationButtonStyle'])) {
546 $buttonStyle = ' style="' . $config['inline']['inlineNewRelationButtonStyle'] . '"';
547 }
548
549 $item = '
550 <a href="#" class="btn btn-default inlineNewRelationButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
551 ' . $buttonStyle . ' onclick="' . htmlspecialchars($onClick) . '" title="' . $createNewRelationText . '">
552 ' . $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render() . '
553 ' . $createNewRelationText . '
554 </a>';
555 $isDirectFileUploadEnabled = (bool)$this->getBackendUserAuthentication()->uc['edit_docModuleUpload'];
556 $allowedArray = GeneralUtility::trimExplode(',', $allowed, true);
557 $onlineMediaAllowed = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
558 if (!empty($allowedArray)) {
559 $onlineMediaAllowed = array_intersect($allowedArray, $onlineMediaAllowed);
560 }
561 if ($showUpload && $isDirectFileUploadEnabled) {
562 $folder = $backendUser->getDefaultUploadFolder(
563 $this->data['parentPageRow']['uid'],
564 $this->data['tableName'],
565 $this->data['fieldName']
566 );
567 if (
568 $folder instanceof Folder
569 && $folder->checkActionPermission('add')
570 ) {
571 $maxFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
572 $item .= ' <a href="#" class="btn btn-default t3js-drag-uploader inlineNewFileUploadButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
573 ' . $buttonStyle . '
574 data-dropzone-target="#' . htmlspecialchars($this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid'])) . '"
575 data-insert-dropzone-before="1"
576 data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
577 data-file-allowed="' . htmlspecialchars($allowed) . '"
578 data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
579 data-max-file-size="' . htmlspecialchars($maxFileSize) . '"
580 ><span class="t3-icon t3-icon-actions t3-icon-actions-edit t3-icon-edit-upload">&nbsp;</span>';
581 $item .= $languageService->sL('LLL:EXT:lang/locallang_core.xlf:file_upload.select-and-submit', true);
582 $item .= '</a>';
583
584 $this->requireJsModules[] = ['TYPO3/CMS/Backend/DragUploader' => 'function(dragUploader){dragUploader.initialize()}'];
585 if (!empty($onlineMediaAllowed)) {
586 $this->requireJsModules[] = 'TYPO3/CMS/Backend/OnlineMedia';
587 $buttonText = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:online_media.new_media.button', true);
588 $placeholder = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:online_media.new_media.placeholder', true);
589 $buttonSubmit = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:online_media.new_media.submit', true);
590 $item .= '
591 <span class="btn btn-default t3js-online-media-add-btn"
592 data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
593 data-online-media-allowed="' . htmlspecialchars(implode(',', $onlineMediaAllowed)) . '"
594 data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
595 title="' . $buttonText . '"
596 data-btn-submit="' . $buttonSubmit . '"
597 data-placeholder="' . $placeholder . '"
598 >
599 ' . $this->iconFactory->getIcon('actions-online-media-add', Icon::SIZE_SMALL)->render() . '
600 ' . $buttonText . '</span>';
601 }
602 }
603 }
604
605 $item = '<div class="form-control-wrap">' . $item . '</div>';
606 $allowedList = '';
607 $allowedLabel = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.allowedFileExtensions', true);
608 foreach ($allowedArray as $allowedItem) {
609 $allowedList .= '<span class="label label-success">' . strtoupper($allowedItem) . '</span> ';
610 }
611 if (!empty($allowedList)) {
612 $item .= '<div class="help-block">' . $allowedLabel . '<br>' . $allowedList . '</div>';
613 }
614 $item = '<div class="form-group t3js-formengine-validation-marker">' . $item . '</div>';
615 return $item;
616 }
617
618 /**
619 * Get a selector as used for the select type, to select from all available
620 * records and to create a relation to the embedding record (e.g. like MM).
621 *
622 * @param array $selItems Array of all possible records
623 * @param array $conf TCA configuration of the parent(!) field
624 * @param array $PA An array with additional configuration options
625 * @param array $uniqueIds The uids that have already been used and should be unique
626 * @return string A HTML <select> box with all possible records
627 */
628 protected function renderPossibleRecordsSelectorTypeSelect($selItems, $conf, &$PA, $uniqueIds = array())
629 {
630 $foreign_table = $conf['foreign_table'];
631 $foreign_selector = $conf['foreign_selector'];
632 $PA = array();
633 $PA['fieldConf'] = $GLOBALS['TCA'][$foreign_table]['columns'][$foreign_selector];
634 $PA['fieldTSConfig'] = FormEngineUtility::getTSconfigForTableRow($foreign_table, array(), $foreign_selector);
635 $config = $PA['fieldConf']['config'];
636 $item = '';
637 // @todo $disabled is not present - should be read from config?
638 $disabled = false;
639 if (!$disabled) {
640 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
641 ;
642 // Create option tags:
643 $opt = array();
644 foreach ($selItems as $p) {
645 if (!in_array($p[1], $uniqueIds)) {
646 $opt[] = '<option value="' . htmlspecialchars($p[1]) . '">' . htmlspecialchars($p[0]) . '</option>';
647 }
648 }
649 // Put together the selector box:
650 $itemListStyle = isset($config['itemListStyle']) ? ' style="' . htmlspecialchars($config['itemListStyle']) . '"' : '';
651 $size = (int)$conf['size'];
652 $size = $conf['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($selItems) + 1, MathUtility::forceIntegerInRange($size, 1), $conf['autoSizeMax']) : $size;
653 $onChange = 'return inline.importNewRecord(' . GeneralUtility::quoteJSvalue($nameObject . '-' . $conf['foreign_table']) . ')';
654 $item = '
655 <select id="' . $nameObject . '-' . $conf['foreign_table'] . '_selector" class="form-control"' . ($size ? ' size="' . $size . '"' : '') . ' onchange="' . htmlspecialchars($onChange) . '"' . $PA['onFocus'] . $itemListStyle . ($conf['foreign_unique'] ? ' isunique="isunique"' : '') . '>
656 ' . implode('', $opt) . '
657 </select>';
658
659 if ($size <= 1) {
660 // Add a "Create new relation" link for adding new relations
661 // This is necessary, if the size of the selector is "1" or if
662 // there is only one record item in the select-box, that is selected by default
663 // The selector-box creates a new relation on using an onChange event (see some line above)
664 if (!empty($conf['appearance']['createNewRelationLinkTitle'])) {
665 $createNewRelationText = $this->getLanguageService()->sL($conf['appearance']['createNewRelationLinkTitle'], true);
666 } else {
667 $createNewRelationText = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', true);
668 }
669 $item .= '
670 <span class="input-group-btn">
671 <a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onChange) . '" title="' . $createNewRelationText . '">
672 ' . $this->iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render() . $createNewRelationText . '
673 </a>
674 </span>';
675 } else {
676 $item .= '
677 <span class="input-group-btn btn"></span>';
678 }
679
680 // Wrap the selector and add a spacer to the bottom
681
682 $item = '<div class="input-group form-group t3js-formengine-validation-marker ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>';
683 }
684 return $item;
685 }
686
687 /**
688 * Extracts FlexForm parts of a form element name like
689 * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
690 * Helper method used in inline
691 *
692 * @param string $formElementName The form element name
693 * @return array|NULL
694 */
695 protected function extractFlexFormParts($formElementName)
696 {
697 $flexFormParts = null;
698 $matches = array();
699 if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
700 $flexFormParts = GeneralUtility::trimExplode(
701 '][',
702 trim($matches[1], '[]')
703 );
704 }
705 return $flexFormParts;
706 }
707
708 /**
709 * @return BackendUserAuthentication
710 */
711 protected function getBackendUserAuthentication()
712 {
713 return $GLOBALS['BE_USER'];
714 }
715
716 /**
717 * @return LanguageService
718 */
719 protected function getLanguageService()
720 {
721 return $GLOBALS['LANG'];
722 }
723 }