Passed
Branch master (6c65a4)
by Christian
16:31
created

getBackendUserAuthentication()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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