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