Passed
Push — master ( 1529e9...26dcf8 )
by
unknown
17:21
created

InlineControlContainer::wrapWithButton()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 2
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Backend\Form\Container;
17
18
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
19
use TYPO3\CMS\Backend\Form\NodeFactory;
20
use TYPO3\CMS\Backend\Utility\BackendUtility;
21
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
22
use TYPO3\CMS\Core\Imaging\Icon;
23
use TYPO3\CMS\Core\Imaging\IconFactory;
24
use TYPO3\CMS\Core\Localization\LanguageService;
25
use TYPO3\CMS\Core\Resource\Folder;
26
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28
use TYPO3\CMS\Core\Utility\MathUtility;
29
use TYPO3\CMS\Core\Utility\StringUtility;
30
31
/**
32
 * Inline element entry container.
33
 *
34
 * This container is the entry step to rendering an inline element. It is created by SingleFieldContainer.
35
 *
36
 * The code creates the main structure for the single inline elements, initializes
37
 * the inlineData array, that is manipulated and also returned back in its manipulated state.
38
 * The "control" stuff of inline elements is rendered here, for example the "create new" button.
39
 *
40
 * For each existing inline relation an InlineRecordContainer is called for further processing.
41
 */
42
class InlineControlContainer extends AbstractContainer
43
{
44
    /**
45
     * Inline data array used in JS, returned as JSON object to frontend
46
     *
47
     * @var array
48
     */
49
    protected $inlineData = [];
50
51
    /**
52
     * @var InlineStackProcessor
53
     */
54
    protected $inlineStackProcessor;
55
56
    /**
57
     * @var IconFactory
58
     */
59
    protected $iconFactory;
60
61
    /**
62
     * @var string[]
63
     */
64
    protected $requireJsModules = [];
65
66
    /**
67
     * Default field information enabled for this element.
68
     *
69
     * @var array
70
     */
71
    protected $defaultFieldInformation = [
72
        'tcaDescription' => [
73
            'renderType' => 'tcaDescription',
74
        ],
75
    ];
76
77
    /**
78
     * @var array Default wizards
79
     */
80
    protected $defaultFieldWizard = [
81
        'localizationStateSelector' => [
82
            'renderType' => 'localizationStateSelector',
83
        ],
84
    ];
85
86
    /**
87
     * Container objects give $nodeFactory down to other containers.
88
     *
89
     * @param NodeFactory $nodeFactory
90
     * @param array $data
91
     */
92
    public function __construct(NodeFactory $nodeFactory, array $data)
93
    {
94
        parent::__construct($nodeFactory, $data);
95
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
96
    }
97
98
    /**
99
     * Entry method
100
     *
101
     * @return array As defined in initializeResultArray() of AbstractNode
102
     */
103
    public function render()
104
    {
105
        $languageService = $this->getLanguageService();
106
107
        $this->inlineData = $this->data['inlineData'];
108
109
        /** @var InlineStackProcessor $inlineStackProcessor */
110
        $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
111
        $this->inlineStackProcessor = $inlineStackProcessor;
112
        $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
113
114
        $table = $this->data['tableName'];
115
        $row = $this->data['databaseRow'];
116
        $field = $this->data['fieldName'];
117
        $parameterArray = $this->data['parameterArray'];
118
119
        $resultArray = $this->initializeResultArray();
120
121
        $config = $parameterArray['fieldConf']['config'];
122
        $foreign_table = $config['foreign_table'];
123
        $isReadOnly = isset($config['readOnly']) && $config['readOnly'];
124
        $language = 0;
125
        $languageFieldName = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
126
        if (BackendUtility::isTableLocalizable($table)) {
127
            $language = isset($row[$languageFieldName][0]) ? (int)$row[$languageFieldName][0] : (int)$row[$languageFieldName];
128
        }
129
130
        // Add the current inline job to the structure stack
131
        $newStructureItem = [
132
            'table' => $table,
133
            'uid' => $row['uid'],
134
            'field' => $field,
135
            'config' => $config,
136
        ];
137
        // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
138
        if (!empty($parameterArray['itemFormElName'])) {
139
            $flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']);
140
            if ($flexFormParts !== null) {
141
                $newStructureItem['flexform'] = $flexFormParts;
142
            }
143
        }
144
        $inlineStackProcessor->pushStableStructureItem($newStructureItem);
145
146
        // Transport the flexform DS identifier fields to the FormInlineAjaxController
147
        if (!empty($newStructureItem['flexform'])
148
            && isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'])
149
        ) {
150
            $config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'];
151
        }
152
153
        // Hand over original returnUrl to FormInlineAjaxController. Needed if opening for instance a
154
        // nested element in a new view to then go back to the original returnUrl and not the url of
155
        // the inline ajax controller
156
        $config['originalReturnUrl'] = $this->data['returnUrl'];
157
158
        // e.g. data[<table>][<uid>][<field>]
159
        $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix();
160
        // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
161
        $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
162
163
        $config['inline']['first'] = false;
164
        $firstChild = reset($this->data['parameterArray']['fieldConf']['children']);
165
        if (isset($firstChild['databaseRow']['uid'])) {
166
            $config['inline']['first'] = $firstChild['databaseRow']['uid'];
167
        }
168
        $config['inline']['last'] = false;
169
        $lastChild = end($this->data['parameterArray']['fieldConf']['children']);
170
        if (isset($lastChild['databaseRow']['uid'])) {
171
            $config['inline']['last'] = $lastChild['databaseRow']['uid'];
172
        }
173
174
        $top = $inlineStackProcessor->getStructureLevel(0);
175
176
        $this->inlineData['config'][$nameObject] = [
177
            'table' => $foreign_table,
178
        ];
179
        $configJson = (string)json_encode($config);
180
        $this->inlineData['config'][$nameObject . '-' . $foreign_table] = [
181
            'min' => $config['minitems'],
182
            'max' => $config['maxitems'],
183
            'sortable' => $config['appearance']['useSortable'],
184
            'top' => [
185
                'table' => $top['table'],
186
                'uid' => $top['uid']
187
            ],
188
            'context' => [
189
                'config' => $configJson,
190
                'hmac' => GeneralUtility::hmac($configJson, 'InlineContext'),
191
            ],
192
        ];
193
        $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
194
195
        $uniqueMax = 0;
196
        $uniqueIds = [];
197
198
        if ($config['foreign_unique']) {
199
            // Add inlineData['unique'] with JS unique configuration
200
            // @todo: Improve validation and throw an exception if type is neither select nor group here
201
            $type = $config['selectorOrUniqueConfiguration']['config']['type'] === 'select' ? 'select' : 'groupdb';
202
            foreach ($parameterArray['fieldConf']['children'] as $child) {
203
                // Determine used unique ids, skip not localized records
204
                if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
205
                    $value = $child['databaseRow'][$config['foreign_unique']];
206
                    // We're assuming there is only one connected value here for both select and group
207
                    if ($type === 'select') {
208
                        // A select field is an array of uids. See TcaSelectItems data provider for details.
209
                        // Pick first entry, ends up as eg. $value = 42.
210
                        $value = $value['0'];
211
                    } else {
212
                        // A group field is an array of arrays containing uid + table + title + row.
213
                        // See TcaGroup data provider for details.
214
                        // Pick the first one (always on 0), and use uid + table only. Exclude title + row
215
                        // since the entire inlineData['unique'] array ends up in JavaScript in the end
216
                        // and we don't need and want the title and the entire row data in the frontend.
217
                        // Ends up as $value = [ 'uid' => '42', 'table' => 'tx_my_table' ]
218
                        $value = [
219
                            'uid' => $value[0]['uid'],
220
                            'table' => $value[0]['table'],
221
                        ];
222
                    }
223
                    // Note structure of $value is different in select vs. group: It's a uid for select, but an
224
                    // array with uid + table for group.
225
                    $uniqueIds[$child['databaseRow']['uid']] = $value;
226
                }
227
            }
228
            $possibleRecords = $config['selectorOrUniquePossibleRecords'];
229
            $possibleRecordsUidToTitle = [];
230
            foreach ($possibleRecords as $possibleRecord) {
231
                $possibleRecordsUidToTitle[$possibleRecord[1]] = $possibleRecord[0];
232
            }
233
            $uniqueMax = $config['appearance']['useCombination'] || empty($possibleRecords) ? -1 : count($possibleRecords);
234
            $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = [
235
                'max' => $uniqueMax,
236
                'used' => $uniqueIds,
237
                'type' => $type,
238
                'table' => $foreign_table,
239
                'elTable' => $config['selectorOrUniqueConfiguration']['foreignTable'],
240
                'field' => $config['foreign_unique'],
241
                'selector' => $config['selectorOrUniqueConfiguration']['isSelector'] ? $type : false,
242
                'possible' => $possibleRecordsUidToTitle,
243
            ];
244
        }
245
246
        $resultArray['inlineData'] = $this->inlineData;
247
248
        // @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider
249
        $uidOfDefaultRecord = $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
250
        $isLocalizedParent = $language > 0
251
            && ($uidOfDefaultRecord[0] ?? $uidOfDefaultRecord) > 0
252
            && MathUtility::canBeInterpretedAsInteger($row['uid']);
253
        $numberOfFullLocalizedChildren = 0;
254
        $numberOfNotYetLocalizedChildren = 0;
255
        foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
256
            if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
257
                $numberOfFullLocalizedChildren++;
258
            }
259
            if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
260
                $numberOfNotYetLocalizedChildren++;
261
            }
262
        }
263
264
        // Render the localization buttons if needed
265
        $localizationButtons = '';
266
        if ($numberOfNotYetLocalizedChildren) {
267
            // Add the "Localize all records" button before all child records:
268
            if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) {
269
                $localizationButtons = ' ' . $this->getLevelInteractionButton('localize', $config);
270
            }
271
            // Add the "Synchronize with default language" button before all child records:
272
            if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) {
273
                $localizationButtons .= ' ' . $this->getLevelInteractionButton('synchronize', $config);
274
            }
275
        }
276
277
        // Define how to show the "Create new record" button - if there are more than maxitems, hide it
278
        if ($isReadOnly || $numberOfFullLocalizedChildren >= $config['maxitems'] || ($uniqueMax > 0 && $numberOfFullLocalizedChildren >= $uniqueMax)) {
279
            $config['inline']['inlineNewButtonStyle'] = 'display: none;';
280
            $config['inline']['inlineNewRelationButtonStyle'] = 'display: none;';
281
            $config['inline']['inlineOnlineMediaAddButtonStyle'] = 'display: none;';
282
        }
283
284
        // Render the level buttons (create new record):
285
        $levelButtons = $this->getLevelInteractionButton('newRecord', $config);
286
287
        $formGroupAttributes = [
288
            'class' => 'form-group',
289
            'id' => $nameObject,
290
            'data-uid' => (string)$row['uid'],
291
            'data-local-table' => (string)$top['table'],
292
            'data-local-field' => (string)$top['field'],
293
            'data-foreign-table' => (string)$foreign_table,
294
            'data-object-group' => $nameObject . '-' . $foreign_table,
295
            'data-form-field' => $nameForm,
296
            'data-appearance' => (string)json_encode($config['appearance']),
297
        ];
298
299
        // Wrap all inline fields of a record with a <div> (like a container)
300
        $html = '<div ' . GeneralUtility::implodeAttributes($formGroupAttributes, true) . '>';
301
302
        $fieldInformationResult = $this->renderFieldInformation();
303
        $html .= $fieldInformationResult['html'];
304
        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
305
306
        // Add the level buttons before all child records:
307
        if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
308
            $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelButtons . $localizationButtons . '</div>';
309
        }
310
311
        // If it's required to select from possible child records (reusable children), add a selector box
312
        if (!$isReadOnly && $config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== false) {
313
            if ($config['selectorOrUniqueConfiguration']['config']['type'] === 'select') {
314
                $selectorBox = $this->renderPossibleRecordsSelectorTypeSelect($config, $uniqueIds);
315
            } else {
316
                $selectorBox = $this->renderPossibleRecordsSelectorTypeGroupDB($config);
317
            }
318
            $html .= $selectorBox . $localizationButtons;
319
        }
320
321
        $title = $languageService->sL(trim($parameterArray['fieldConf']['label']));
322
        $html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">';
323
324
        $sortableRecordUids = [];
325
        foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) {
326
            $options['inlineParentUid'] = $row['uid'];
327
            $options['inlineFirstPid'] = $this->data['inlineFirstPid'];
328
            // @todo: this can be removed if this container no longer sets additional info to $config
329
            $options['inlineParentConfig'] = $config;
330
            $options['inlineData'] = $this->inlineData;
331
            $options['inlineStructure'] = $inlineStackProcessor->getStructure();
332
            $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
333
            $options['renderType'] = 'inlineRecordContainer';
334
            $childResult = $this->nodeFactory->create($options)->render();
335
            $html .= $childResult['html'];
336
            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
337
            if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
338
                // Don't add record to list of "valid" uids if it is only the default
339
                // language record of a not yet localized child
340
                $sortableRecordUids[] = $options['databaseRow']['uid'];
341
            }
342
        }
343
344
        $html .= '</div>';
345
346
        $fieldWizardResult = $this->renderFieldWizard();
347
        $fieldWizardHtml = $fieldWizardResult['html'];
348
        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
349
        $html .= $fieldWizardHtml;
350
351
        // Add the level buttons after all child records:
352
        if (!$isReadOnly && ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom')) {
353
            $html .= $levelButtons . $localizationButtons;
354
        }
355
        if (is_array($config['customControls'])) {
356
            $html .= '<div id="' . $nameObject . '_customControls">';
357
            foreach ($config['customControls'] as $customControlConfig) {
358
                if (!isset($customControlConfig['userFunc'])) {
359
                    throw new \RuntimeException('Support for customControl without a userFunc key in TCA type inline is not supported.', 1548052629);
360
                }
361
                $parameters = [
362
                    'table' => $table,
363
                    'field' => $field,
364
                    'row' => $row,
365
                    'nameObject' => $nameObject,
366
                    'nameForm' => $nameForm,
367
                    'config' => $config,
368
                    'customControlConfig' => $customControlConfig,
369
                ];
370
                $html .= GeneralUtility::callUserFunction($customControlConfig['userFunc'], $parameters, $this);
371
            }
372
            $html .= '</div>';
373
        }
374
        $resultArray['requireJsModules'] = array_merge($resultArray['requireJsModules'], $this->requireJsModules);
375
        $resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Container/InlineControlContainer' => '
376
            function(InlineControlContainer) {
377
                new InlineControlContainer(' . GeneralUtility::quoteJSvalue($nameObject) . ');
378
            }'
379
        ];
380
381
        // Publish the uids of the child records in the given order to the browser
382
        $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
383
            . ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString(['type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']])) . '"'
0 ignored issues
show
Bug introduced by
The method getValidationDataAsJsonString() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

383
            . ' data-formengine-validation-rules="' . htmlspecialchars($this->/** @scrutinizer ignore-call */ getValidationDataAsJsonString(['type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']])) . '"'

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
384
            . ' class="inlineRecord" />';
385
        // Close the wrap for all inline fields (container)
386
        $html .= '</div>';
387
388
        $resultArray['html'] = $html;
389
        return $resultArray;
390
    }
391
392
    /**
393
     * Creates the HTML code of a general button to be used on a level of inline children.
394
     * The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'.
395
     *
396
     * @param string $type The button type, values are 'newRecord', 'localize' and 'synchronize'.
397
     * @param array $conf TCA configuration of the parent(!) field
398
     * @return string The HTML code of the new button, wrapped in a div
399
     */
400
    protected function getLevelInteractionButton(string $type, array $conf = []): string
401
    {
402
        $languageService = $this->getLanguageService();
403
        $attributes = [];
404
        switch ($type) {
405
            case 'newRecord':
406
                $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createnew'));
407
                $icon = 'actions-add';
408
                $className = 'typo3-newRecordLink t3js-inline-controls';
409
                $attributes['class'] = 'btn btn-default t3js-create-new-button';
410
                if (!empty($conf['inline']['inlineNewButtonStyle'])) {
411
                    $attributes['style'] = $conf['inline']['inlineNewButtonStyle'];
412
                }
413
                if (!empty($conf['appearance']['newRecordLinkAddTitle'])) {
414
                    $title = htmlspecialchars(sprintf(
415
                        $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createnew.link'),
416
                        $languageService->sL($GLOBALS['TCA'][$conf['foreign_table']]['ctrl']['title'])
417
                    ));
418
                } elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') {
419
                    $title = htmlspecialchars($languageService->sL($conf['appearance']['newRecordLinkTitle']));
420
                }
421
                break;
422
            case 'localize':
423
                $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localizeAllRecords'));
424
                $icon = 'actions-document-localize';
425
                $className = 'typo3-localizationLink';
426
                $attributes['class'] = 'btn btn-default t3js-synchronizelocalize-button';
427
                $attributes['data-type'] = 'localize';
428
                break;
429
            case 'synchronize':
430
                $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:synchronizeWithOriginalLanguage'));
431
                $icon = 'actions-document-synchronize';
432
                $className = 'typo3-synchronizationLink';
433
                $attributes['class'] = 'btn btn-default inlineNewButton t3js-synchronizelocalize-button';
434
                $attributes['data-type'] = 'synchronize';
435
                break;
436
            default:
437
                $title = '';
438
                $icon = '';
439
                $className = '';
440
        }
441
        // Create the button:
442
        $icon = $icon ? $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() : '';
443
        $button = $this->wrapWithButton($icon . ' ' . $title, $attributes);
444
        return '<div' . ($className ? ' class="' . $className . '"' : '') . 'title="' . $title . '">' . $button . '</div>';
445
    }
446
447
    /**
448
     * Wraps a text with a button and returns the HTML representation.
449
     *
450
     * @param string $text The text to be wrapped by a button
451
     * @param array<string, string> $attributes Array of attributes to be used in the anchor
452
     * @return string The wrapped text as HTML representation
453
     */
454
    protected function wrapWithButton(string $text, array $attributes = []): string
455
    {
456
        return '<button type="button" ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>' . $text . '</button>';
457
    }
458
459
    /**
460
     * Generate a button that opens an element browser in a new window.
461
     * For group/db there is no way to use a "selector" like a <select>|</select>-box.
462
     *
463
     * @param array $inlineConfiguration TCA inline configuration of the parent(!) field
464
     * @return string A HTML button that opens an element browser in a new window
465
     */
466
    protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration)
467
    {
468
        $backendUser = $this->getBackendUserAuthentication();
469
        $languageService = $this->getLanguageService();
470
471
        $groupFieldConfiguration = $inlineConfiguration['selectorOrUniqueConfiguration']['config'];
472
473
        $foreign_table = $inlineConfiguration['foreign_table'];
474
        $allowed = $groupFieldConfiguration['allowed'];
475
        $currentStructureDomObjectIdPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
476
        $objectPrefix = $currentStructureDomObjectIdPrefix . '-' . $foreign_table;
477
        $mode = 'db';
478
        $showUpload = false;
479
        $showByUrl = false;
480
        $elementBrowserEnabled = true;
481
        if (!empty($inlineConfiguration['appearance']['createNewRelationLinkTitle'])) {
482
            $createNewRelationText = htmlspecialchars($languageService->sL($inlineConfiguration['appearance']['createNewRelationLinkTitle']));
483
        } else {
484
            $createNewRelationText = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation'));
485
        }
486
        if (is_array($groupFieldConfiguration['appearance'])) {
487
            if (isset($groupFieldConfiguration['appearance']['elementBrowserType'])) {
488
                $mode = $groupFieldConfiguration['appearance']['elementBrowserType'];
489
            }
490
            if ($mode === 'file') {
491
                $showUpload = true;
492
                $showByUrl = true;
493
            }
494
            if (isset($inlineConfiguration['appearance']['fileUploadAllowed'])) {
495
                $showUpload = (bool)$inlineConfiguration['appearance']['fileUploadAllowed'];
496
            }
497
            if (isset($inlineConfiguration['appearance']['fileByUrlAllowed'])) {
498
                $showByUrl = (bool)$inlineConfiguration['appearance']['fileByUrlAllowed'];
499
            }
500
            if (isset($groupFieldConfiguration['appearance']['elementBrowserAllowed'])) {
501
                $allowed = $groupFieldConfiguration['appearance']['elementBrowserAllowed'];
502
            }
503
            if (isset($inlineConfiguration['appearance']['elementBrowserEnabled'])) {
504
                $elementBrowserEnabled = (bool)$inlineConfiguration['appearance']['elementBrowserEnabled'];
505
            }
506
        }
507
        // Remove any white-spaces from the allowed extension lists
508
        $allowed = implode(',', GeneralUtility::trimExplode(',', $allowed, true));
509
        $browserParams = '|||' . $allowed . '|' . $objectPrefix;
510
        $buttonStyle = '';
511
        if (isset($inlineConfiguration['inline']['inlineNewRelationButtonStyle'])) {
512
            $buttonStyle = ' style="' . $inlineConfiguration['inline']['inlineNewRelationButtonStyle'] . '"';
513
        }
514
        $item = '';
515
        if ($elementBrowserEnabled) {
516
            $item .= '
517
			<button type="button" class="btn btn-default t3js-element-browser" data-mode="' . htmlspecialchars($mode) . '" data-params="' . htmlspecialchars($browserParams) . '"
518
				' . $buttonStyle . ' title="' . $createNewRelationText . '">
519
				' . $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render() . '
520
				' . $createNewRelationText . '
521
			</button>';
522
        }
523
524
        $isDirectFileUploadEnabled = (bool)$backendUser->uc['edit_docModuleUpload'];
525
        $allowedArray = GeneralUtility::trimExplode(',', $allowed, true);
526
        $onlineMediaAllowed = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
527
        if (!empty($allowedArray)) {
528
            $onlineMediaAllowed = array_intersect($allowedArray, $onlineMediaAllowed);
529
        }
530
        if (($showUpload || $showByUrl) && $isDirectFileUploadEnabled) {
531
            $folder = $backendUser->getDefaultUploadFolder(
532
                $this->data['tableName'] === 'pages' ? $this->data['vanillaUid'] : $this->data['parentPageRow']['uid'],
533
                $this->data['tableName'],
534
                $this->data['fieldName']
535
            );
536
            if (
537
                $folder instanceof Folder
538
                && $folder->getStorage()->checkUserActionPermission('add', 'File')
539
            ) {
540
                if ($showUpload) {
541
                    $maxFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
542
                    $item .= ' <button type="button" class="btn btn-default t3js-drag-uploader inlineNewFileUploadButton"
543
					' . $buttonStyle . '
544
					data-dropzone-target="#' . htmlspecialchars(StringUtility::escapeCssSelector($currentStructureDomObjectIdPrefix)) . '"
545
					data-insert-dropzone-before="1"
546
					data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
547
					data-file-allowed="' . htmlspecialchars($allowed) . '"
548
					data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
549
					data-max-file-size="' . htmlspecialchars((string)$maxFileSize) . '"
550
					>';
551
                    $item .= $this->iconFactory->getIcon('actions-upload', Icon::SIZE_SMALL)->render() . ' ';
552
                    $item .= htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.select-and-submit'));
553
                    $item .= '</button>';
554
555
                    $this->requireJsModules[] = ['TYPO3/CMS/Backend/DragUploader' => 'function(dragUploader){dragUploader.initialize()}'];
556
                }
557
                if (!empty($onlineMediaAllowed) && $showByUrl) {
558
                    $buttonStyle = '';
559
                    if (isset($inlineConfiguration['inline']['inlineOnlineMediaAddButtonStyle'])) {
560
                        $buttonStyle = ' style="' . $inlineConfiguration['inline']['inlineOnlineMediaAddButtonStyle'] . '"';
561
                    }
562
                    $this->requireJsModules[] = 'TYPO3/CMS/Backend/OnlineMedia';
563
                    $buttonText = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.button'));
564
                    $placeholder = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.placeholder'));
565
                    $buttonSubmit = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.submit'));
566
                    $allowedMediaUrl = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.allowEmbedSources'));
567
                    $item .= '
568
						<button type="button" class="btn btn-default t3js-online-media-add-btn"
569
							' . $buttonStyle . '
570
							data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
571
							data-online-media-allowed="' . htmlspecialchars(implode(',', $onlineMediaAllowed)) . '"
572
							data-online-media-allowed-help-text="' . $allowedMediaUrl . '"
573
							data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
574
							title="' . $buttonText . '"
575
							data-btn-submit="' . $buttonSubmit . '"
576
							data-placeholder="' . $placeholder . '"
577
							>
578
							' . $this->iconFactory->getIcon('actions-online-media-add', Icon::SIZE_SMALL)->render() . '
579
							' . $buttonText . '</button>';
580
                }
581
            }
582
        }
583
584
        $item = '<div class="form-control-wrap t3js-inline-controls">' . $item . '</div>';
585
        $allowedList = '';
586
        $allowedLabelKey = ($mode === 'file') ? 'allowedFileExtensions' : 'allowedRelations';
587
        $allowedLabel = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . $allowedLabelKey));
588
        foreach ($allowedArray as $allowedItem) {
589
            $allowedList .= '<span class="label label-success">' . strtoupper($allowedItem) . '</span> ';
590
        }
591
        if (!empty($allowedList)) {
592
            $item .= '<div class="help-block">' . $allowedLabel . '<br>' . $allowedList . '</div>';
593
        }
594
        $item = '<div class="form-group t3js-formengine-validation-marker">' . $item . '</div>';
595
        return $item;
596
    }
597
598
    /**
599
     * Get a selector as used for the select type, to select from all available
600
     * records and to create a relation to the embedding record (e.g. like MM).
601
     *
602
     * @param array $config TCA inline configuration of the parent(!) field
603
     * @param array $uniqueIds The uids that have already been used and should be unique
604
     * @return string A HTML <select> box with all possible records
605
     */
606
    protected function renderPossibleRecordsSelectorTypeSelect(array $config, array $uniqueIds)
607
    {
608
        $possibleRecords = $config['selectorOrUniquePossibleRecords'];
609
        $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
610
        // Create option tags:
611
        $opt = [];
612
        foreach ($possibleRecords as $p) {
613
            if (!in_array($p[1], $uniqueIds)) {
614
                $opt[] = '<option value="' . htmlspecialchars($p[1]) . '">' . htmlspecialchars($p[0]) . '</option>';
615
            }
616
        }
617
        // Put together the selector box:
618
        $size = (int)$config['size'];
619
        $size = $config['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($possibleRecords) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax']) : $size;
620
        $item = '
621
            <select id="' . $nameObject . '-' . $config['foreign_table'] . '_selector" class="form-control t3js-create-new-selector"' . ($size ? ' size="' . $size . '"' : '') . '>
622
                ' . implode('', $opt) . '
623
            </select>';
624
625
        if ($size <= 1) {
626
            // Add a "Create new relation" button for adding new relations
627
            // This is necessary, if the size of the selector is "1" or if
628
            // there is only one record item in the select-box, that is selected by default
629
            // The selector-box creates a new relation on using an onChange event (see some line above)
630
            if (!empty($config['appearance']['createNewRelationLinkTitle'])) {
631
                $createNewRelationText = htmlspecialchars($this->getLanguageService()->sL($config['appearance']['createNewRelationLinkTitle']));
632
            } else {
633
                $createNewRelationText = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation'));
634
            }
635
            $item .= '
636
            <span class="input-group-btn">
637
                <button type="button" class="btn btn-default t3js-create-new-button" title="' . $createNewRelationText . '">
638
                    ' . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . $createNewRelationText . '
639
                </button>
640
            </span>';
641
        } else {
642
            $item .= '
643
            <span class="input-group-btn btn"></span>';
644
        }
645
646
        // Wrap the selector and add a spacer to the bottom
647
        $item = '<div class="input-group form-group t3js-formengine-validation-marker">' . $item . '</div>';
648
        return $item;
649
    }
650
651
    /**
652
     * Extracts FlexForm parts of a form element name like
653
     * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
654
     * Helper method used in inline
655
     *
656
     * @param string $formElementName The form element name
657
     * @return array|null
658
     */
659
    protected function extractFlexFormParts($formElementName)
660
    {
661
        $flexFormParts = null;
662
        $matches = [];
663
        if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
664
            $flexFormParts = GeneralUtility::trimExplode(
665
                '][',
666
                trim($matches[1], '[]')
667
            );
668
        }
669
        return $flexFormParts;
670
    }
671
672
    /**
673
     * @return BackendUserAuthentication
674
     */
675
    protected function getBackendUserAuthentication()
676
    {
677
        return $GLOBALS['BE_USER'];
678
    }
679
680
    /**
681
     * @return LanguageService
682
     */
683
    protected function getLanguageService()
684
    {
685
        return $GLOBALS['LANG'];
686
    }
687
}
688