Test Failed
Branch master (7b1793)
by Tymoteusz
15:35
created

InlineRecordContainer::renderCombinationChild()   C

Complexity

Conditions 7
Paths 12

Size

Total Lines 48
Code Lines 32

Duplication

Lines 8
Ratio 16.67 %

Importance

Changes 0
Metric Value
cc 7
eloc 32
nc 12
nop 2
dl 8
loc 48
rs 6.7272
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\Element\InlineElementHookInterface;
18
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedContentEditException;
19
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
20
use TYPO3\CMS\Backend\Form\NodeFactory;
21
use TYPO3\CMS\Backend\Utility\BackendUtility;
22
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\Imaging\Icon;
25
use TYPO3\CMS\Core\Imaging\IconFactory;
26
use TYPO3\CMS\Core\Localization\LanguageService;
27
use TYPO3\CMS\Core\Messaging\FlashMessage;
28
use TYPO3\CMS\Core\Resource\ProcessedFile;
29
use TYPO3\CMS\Core\Resource\ResourceFactory;
30
use TYPO3\CMS\Core\Type\Bitmask\Permission;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
use TYPO3\CMS\Core\Utility\MathUtility;
33
34
/**
35
 * Render a single inline record relation.
36
 *
37
 * This container is called by InlineControlContainer to render single existing records.
38
 * Furthermore it is called by FormEngine for an incoming ajax request to expand an existing record
39
 * or to create a new one.
40
 *
41
 * This container creates the outer HTML of single inline records - eg. drag and drop and delete buttons.
42
 * For rendering of the record itself processing is handed over to FullRecordContainer.
43
 */
44
class InlineRecordContainer extends AbstractContainer
45
{
46
    /**
47
     * Inline data array used for JSON output
48
     *
49
     * @var array
50
     */
51
    protected $inlineData = [];
52
53
    /**
54
     * @var InlineStackProcessor
55
     */
56
    protected $inlineStackProcessor;
57
58
    /**
59
     * Array containing instances of hook classes called once for IRRE objects
60
     *
61
     * @var array
62
     */
63
    protected $hookObjects = [];
64
65
    /**
66
     * @var IconFactory
67
     */
68
    protected $iconFactory;
69
70
    /**
71
     * Default constructor
72
     *
73
     * @param NodeFactory $nodeFactory
74
     * @param array $data
75
     */
76
    public function __construct(NodeFactory $nodeFactory, array $data)
77
    {
78
        parent::__construct($nodeFactory, $data);
79
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
80
        $this->inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
81
        $this->initHookObjects();
82
    }
83
84
    /**
85
     * Entry method
86
     *
87
     * @return array As defined in initializeResultArray() of AbstractNode
88
     * @throws AccessDeniedContentEditException
89
     */
90
    public function render()
91
    {
92
        $data = $this->data;
93
        $this->inlineData = $data['inlineData'];
94
95
        $inlineStackProcessor = $this->inlineStackProcessor;
96
        $inlineStackProcessor->initializeByGivenStructure($data['inlineStructure']);
97
98
        $record = $data['databaseRow'];
99
        $inlineConfig = $data['inlineParentConfig'];
100
        $foreignTable = $inlineConfig['foreign_table'];
101
102
        $resultArray = $this->initializeResultArray();
103
104
        // Send a mapping information to the browser via JSON:
105
        // e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
106
        $formPrefix = $inlineStackProcessor->getCurrentStructureFormPrefix();
107
        $domObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
108
        $this->inlineData['map'][$formPrefix] = $domObjectId;
109
110
        $resultArray['inlineData'] = $this->inlineData;
111
112
        // If there is a selector field, normalize it:
113
        if (!empty($inlineConfig['foreign_selector'])) {
114
            $foreign_selector = $inlineConfig['foreign_selector'];
115
            $valueToNormalize = $record[$foreign_selector];
116
            if (is_array($record[$foreign_selector])) {
117
                // @todo: this can be kicked again if always prepared rows are handled here
118
                $valueToNormalize = implode(',', $record[$foreign_selector]);
119
            }
120
            $record[$foreign_selector] = $this->normalizeUid($valueToNormalize);
121
        }
122
123
        // Get the current naming scheme for DOM name/id attributes:
124
        $appendFormFieldNames = '[' . $foreignTable . '][' . $record['uid'] . ']';
125
        $objectId = $domObjectId . '-' . $foreignTable . '-' . $record['uid'];
126
        $class = '';
127
        $html = '';
128
        $combinationHtml = '';
129
        $isNewRecord = $data['command'] === 'new';
130
        $hiddenField = '';
131
        if (isset($data['processedTca']['ctrl']['enablecolumns']['disabled'])) {
132
            $hiddenField = $data['processedTca']['ctrl']['enablecolumns']['disabled'];
133
        }
134
        if (!$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
135
            if ($isNewRecord || $data['isInlineChildExpanded']) {
136
                // Render full content ONLY IF this is an AJAX request, a new record, or the record is not collapsed
137
                $combinationHtml = '';
138
                if (isset($data['combinationChild'])) {
139
                    $combinationChild = $this->renderCombinationChild($data, $appendFormFieldNames);
140
                    $combinationHtml = $combinationChild['html'];
141
                    $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild, false);
142
                }
143
                $childArray = $this->renderChild($data);
144
                $html = $childArray['html'];
145
                $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
146
            } else {
147
                // This string is the marker for the JS-function to check if the full content has already been loaded
148
                $html = '<!--notloaded-->';
149
            }
150
            if ($isNewRecord) {
151
                // Add pid of record as hidden field
152
                $html .= '<input type="hidden" name="data' . htmlspecialchars($appendFormFieldNames)
153
                    . '[pid]" value="' . htmlspecialchars($record['pid']) . '"/>';
154
                // Tell DataHandler this record is expanded
155
                $ucFieldName = 'uc[inlineView]'
156
                    . '[' . $data['inlineTopMostParentTableName'] . ']'
157
                    . '[' . $data['inlineTopMostParentUid'] . ']'
158
                    . $appendFormFieldNames;
159
                $html .= '<input type="hidden" name="' . htmlspecialchars($ucFieldName)
160
                    . '" value="' . (int)$data['isInlineChildExpanded'] . '" />';
161
            } else {
162
                // Set additional field for processing for saving
163
                $html .= '<input type="hidden" name="cmd' . htmlspecialchars($appendFormFieldNames)
164
                    . '[delete]" value="1" disabled="disabled" />';
165
                if (!$data['isInlineChildExpanded'] && !empty($hiddenField)) {
166
                    $checked = !empty($record[$hiddenField]) ? ' checked="checked"' : '';
167
                    $html .= '<input type="checkbox" data-formengine-input-name="data'
168
                        . htmlspecialchars($appendFormFieldNames)
169
                        . '[' . htmlspecialchars($hiddenField) . ']" value="1"' . $checked . ' />';
170
                    $html .= '<input type="input" name="data' . htmlspecialchars($appendFormFieldNames)
171
                        . '[' . htmlspecialchars($hiddenField) . ']" value="' . htmlspecialchars($record[$hiddenField]) . '" />';
172
                }
173
            }
174
            // If this record should be shown collapsed
175
            $class = $data['isInlineChildExpanded'] ? 'panel-visible' : 'panel-collapsed';
176
        }
177
        if ($inlineConfig['renderFieldsOnly']) {
178
            // Render "body" part only
179
            $html = $html . $combinationHtml;
180
        } else {
181
            // Render header row and content (if expanded)
182
            if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
183
                $class .= ' t3-form-field-container-inline-placeHolder';
184
            }
185
            if (!empty($hiddenField) && isset($record[$hiddenField]) && (int)$record[$hiddenField]) {
186
                $class .= ' t3-form-field-container-inline-hidden';
187
            }
188
            $class .= ($isNewRecord ? ' inlineIsNewRecord' : '');
189
            $html = '
190
				<div class="panel panel-default panel-condensed ' . trim($class) . '" id="' . htmlspecialchars($objectId) . '_div">
191
					<div class="panel-heading" data-toggle="formengine-inline" id="' . htmlspecialchars($objectId) . '_header" data-expandSingle="' . ($inlineConfig['appearance']['expandSingle'] ? 1 : 0) . '">
192
						<div class="form-irre-header">
193
							<div class="form-irre-header-cell form-irre-header-icon">
194
								<span class="caret"></span>
195
							</div>
196
							' . $this->renderForeignRecordHeader($data) . '
197
						</div>
198
					</div>
199
					<div class="panel-collapse" id="' . htmlspecialchars($objectId) . '_fields">' . $html . $combinationHtml . '</div>
200
				</div>';
201
        }
202
203
        $resultArray['html'] = $html;
204
        return $resultArray;
205
    }
206
207
    /**
208
     * Render inner child
209
     *
210
     * @param array $data
211
     * @return array Result array
212
     */
213
    protected function renderChild(array $data)
214
    {
215
        $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
216
        $data['tabAndInlineStack'][] = [
217
            'inline',
218
            $domObjectId . '-' . $data['tableName'] . '-' . $data['databaseRow']['uid'],
219
        ];
220
        // @todo: ugly construct ...
221
        $data['inlineData'] = $this->inlineData;
222
        $data['renderType'] = 'fullRecordContainer';
223
        return $this->nodeFactory->create($data)->render();
224
    }
225
226
    /**
227
     * Render child child
228
     *
229
     * Render a table with FormEngine, that occurs on an intermediate table but should be editable directly,
230
     * so two tables are combined (the intermediate table with attributes and the sub-embedded table).
231
     * -> This is a direct embedding over two levels!
232
     *
233
     * @param array $data
234
     * @param string $appendFormFieldNames The [<table>][<uid>] of the parent record (the intermediate table)
235
     * @return array Result array
236
     */
237
    protected function renderCombinationChild(array $data, $appendFormFieldNames)
238
    {
239
        $childData = $data['combinationChild'];
240
        $parentConfig = $data['inlineParentConfig'];
241
242
        $resultArray = $this->initializeResultArray();
243
244
        // Display Warning FlashMessage if it is not suppressed
245
        if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
246
            $combinationWarningMessage = 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.inline_use_combination';
247
            if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
248
                $combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
249
            }
250
            $message = $this->getLanguageService()->sL($combinationWarningMessage);
251
            $markup = [];
252
            // @TODO: This is not a FlashMessage! The markup must be changed and special CSS
253
            // @TODO: should be created, in order to prevent confusion.
254
            $markup[] = '<div class="alert alert-warning">';
255
            $markup[] = '    <div class="media">';
256
            $markup[] = '        <div class="media-left">';
257
            $markup[] = '            <span class="fa-stack fa-lg">';
258
            $markup[] = '                <i class="fa fa-circle fa-stack-2x"></i>';
259
            $markup[] = '                <i class="fa fa-exclamation fa-stack-1x"></i>';
260
            $markup[] = '            </span>';
261
            $markup[] = '        </div>';
262
            $markup[] = '        <div class="media-body">';
263
            $markup[] = '            <div class="alert-message">' . htmlspecialchars($message) . '</div>';
264
            $markup[] = '        </div>';
265
            $markup[] = '    </div>';
266
            $markup[] = '</div>';
267
            $resultArray['html'] = implode(LF, $markup);
268
        }
269
270
        $childArray = $this->renderChild($childData);
271
        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
272
273
        // If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
274 View Code Duplication
        if ($childData['command'] === 'new') {
275
            $comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
276
            $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
277
        }
278
        // If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
279 View Code Duplication
        if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
280
            $parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
281
            $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
282
        }
283
284
        return $resultArray;
285
    }
286
287
    /**
288
     * Renders the HTML header for a foreign record, such as the title, toggle-function, drag'n'drop, etc.
289
     * Later on the command-icons are inserted here.
290
     *
291
     * @param array $data Current data
292
     * @return string The HTML code of the header
293
     */
294
    protected function renderForeignRecordHeader(array $data)
295
    {
296
        $languageService = $this->getLanguageService();
297
        $inlineConfig = $data['inlineParentConfig'];
298
        $foreignTable = $inlineConfig['foreign_table'];
299
        $rec = $data['databaseRow'];
300
        // Init:
301
        $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
302
        $objectId = $domObjectId . '-' . $foreignTable . '-' . $rec['uid'];
303
304
        $recordTitle = $data['recordTitle'];
305
        if (!empty($recordTitle)) {
306
            // The user function may return HTML, therefore we can't escape it
307
            if (empty($data['processedTca']['ctrl']['formattedLabel_userFunc'])) {
308
                $recordTitle = BackendUtility::getRecordTitlePrep($recordTitle);
309
            }
310
        } else {
311
            $recordTitle = '<em>[' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>';
312
        }
313
314
        $altText = BackendUtility::getRecordIconAltText($rec, $foreignTable);
315
316
        $iconImg = '<span title="' . $altText . '" id="' . htmlspecialchars($objectId) . '_icon' . '">' . $this->iconFactory->getIconForRecord($foreignTable, $rec, Icon::SIZE_SMALL)->render() . '</span>';
317
        $label = '<span id="' . $objectId . '_label">' . $recordTitle . '</span>';
318
        $ctrl = $this->renderForeignRecordHeaderControl($data);
319
        $thumbnail = false;
320
321
        // Renders a thumbnail for the header
322
        if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] && !empty($inlineConfig['appearance']['headerThumbnail']['field'])) {
323
            $fieldValue = $rec[$inlineConfig['appearance']['headerThumbnail']['field']];
324
            $fileUid = $fieldValue[0]['uid'];
325
326
            if (!empty($fileUid)) {
327
                try {
328
                    $fileObject = ResourceFactory::getInstance()->getFileObject($fileUid);
329
                } catch (\InvalidArgumentException $e) {
330
                    $fileObject = null;
331
                }
332
                if ($fileObject && $fileObject->isMissing()) {
333
                    $thumbnail .= '<span class="label label-danger">'
334
                        . htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
0 ignored issues
show
Bug Best Practice introduced by
The method TYPO3\CMS\Backend\Form\C...r::getLanguageService() is not static, but was called statically. ( Ignorable by Annotation )

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

334
                        . htmlspecialchars(static::/** @scrutinizer ignore-call */ getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
Loading history...
335
                        . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
336
                } elseif ($fileObject) {
337
                    $imageSetup = $inlineConfig['appearance']['headerThumbnail'];
338
                    unset($imageSetup['field']);
339
                    if (!empty($rec['crop'])) {
340
                        $imageSetup['crop'] = $rec['crop'];
341
                    }
342
                    $imageSetup = array_merge(['width' => '45', 'height' => '45c'], $imageSetup);
343
344
                    if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
345
                        && GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileObject->getExtension())) {
346
                        $processedImage = $fileObject->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, $imageSetup);
347
                        // Only use a thumbnail if the processing process was successful by checking if image width is set
348
                        if ($processedImage->getProperty('width')) {
349
                            $imageUrl = $processedImage->getPublicUrl(true);
350
                            $thumbnail = '<img src="' . $imageUrl . '" ' .
351
                                'width="' . $processedImage->getProperty('width') . '" ' .
352
                                'height="' . $processedImage->getProperty('height') . '" ' .
353
                                'alt="' . htmlspecialchars($altText) . '" ' .
354
                                'title="' . htmlspecialchars($altText) . '">';
355
                        }
356
                    } else {
357
                        $thumbnail = '';
358
                    }
359
                }
360
            }
361
        }
362
363
        if (!empty($inlineConfig['appearance']['headerThumbnail']['field']) && $thumbnail) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $thumbnail of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
364
            $mediaContainer = '<div class="form-irre-header-cell form-irre-header-thumbnail" id="' . $objectId . '_thumbnailcontainer">' . $thumbnail . '</div>';
365
        } else {
366
            $mediaContainer = '<div class="form-irre-header-cell form-irre-header-icon" id="' . $objectId . '_iconcontainer">' . $iconImg . '</div>';
367
        }
368
        $header = $mediaContainer . '
369
				<div class="form-irre-header-cell form-irre-header-body">' . $label . '</div>
370
				<div class="form-irre-header-cell form-irre-header-control t3js-formengine-irre-control">' . $ctrl . '</div>';
371
372
        return $header;
373
    }
374
375
    /**
376
     * Render the control-icons for a record header (create new, sorting, delete, disable/enable).
377
     * Most of the parts are copy&paste from TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList and
378
     * modified for the JavaScript calls here
379
     *
380
     * @param array $data Current data
381
     * @return string The HTML code with the control-icons
382
     */
383
    protected function renderForeignRecordHeaderControl(array $data)
384
    {
385
        $rec = $data['databaseRow'];
386
        $inlineConfig = $data['inlineParentConfig'];
387
        $foreignTable = $inlineConfig['foreign_table'];
388
        $languageService = $this->getLanguageService();
389
        $backendUser = $this->getBackendUserAuthentication();
390
        // Initialize:
391
        $cells = [
392
            'edit' => '',
393
            'hide' => '',
394
            'delete' => '',
395
            'info' => '',
396
            'new' => '',
397
            'sort.up' => '',
398
            'sort.down' => '',
399
            'dragdrop' => '',
400
            'localize' => '',
401
            'locked' => '',
402
        ];
403
        $isNewItem = substr($rec['uid'], 0, 3) === 'NEW';
404
        $isParentExisting = MathUtility::canBeInterpretedAsInteger($data['inlineParentUid']);
405
        $tcaTableCtrl = $GLOBALS['TCA'][$foreignTable]['ctrl'];
406
        $tcaTableCols = $GLOBALS['TCA'][$foreignTable]['columns'];
407
        $isPagesTable = $foreignTable === 'pages';
408
        $isSysFileReferenceTable = $foreignTable === 'sys_file_reference';
409
        $enableManualSorting = $tcaTableCtrl['sortby'] || $inlineConfig['MM'] || !$data['isOnSymmetricSide']
410
            && $inlineConfig['foreign_sortby'] || $data['isOnSymmetricSide'] && $inlineConfig['symmetric_sortby'];
411
        $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
412
        $nameObjectFt = $nameObject . '-' . $foreignTable;
413
        $nameObjectFtId = $nameObjectFt . '-' . $rec['uid'];
414
        $calcPerms = $backendUser->calcPerms(BackendUtility::readPageAccess($rec['pid'], $backendUser->getPagePermsClause(1)));
415
        // If the listed table is 'pages' we have to request the permission settings for each page:
416
        $localCalcPerms = false;
417
        if ($isPagesTable) {
418
            $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid']));
419
        }
420
        // This expresses the edit permissions for this particular element:
421
        $permsEdit = $isPagesTable && $localCalcPerms & Permission::PAGE_EDIT || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT;
422
        // Controls: Defines which controls should be shown
423
        $enabledControls = $inlineConfig['appearance']['enabledControls'];
424
        // Hook: Can disable/enable single controls for specific child records:
425
        foreach ($this->hookObjects as $hookObj) {
426
            /** @var InlineElementHookInterface $hookObj */
427
            $hookObj->renderForeignRecordHeaderControl_preProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $enabledControls);
428
        }
429
        if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
430
            $cells['localize'] = '<span title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:localize.isLocalizable')) . '">
431
                    ' . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render() . '
432
                </span>';
433
        }
434
        // "Info": (All records)
435
        // @todo: hardcoded sys_file!
436
        if ($rec['table_local'] === 'sys_file') {
437
            $uid = $rec['uid_local'][0]['uid'];
438
            $table = '_FILE';
439
        } else {
440
            $uid = $rec['uid'];
441
            $table = $foreignTable;
442
        }
443
        if ($enabledControls['info']) {
444
            if ($isNewItem) {
445
                $cells['info'] = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
446
            } else {
447
                $cells['info'] = '
448
				<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(('top.launchView(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($uid) . '); return false;')) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')) . '">
449
					' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '
450
				</a>';
451
            }
452
        }
453
        // If the table is NOT a read-only table, then show these links:
454
        if (!$tcaTableCtrl['readOnly'] && !$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
455
            // "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row or if default values can depend on previous record):
456
            if ($enabledControls['new'] && ($enableManualSorting || $tcaTableCtrl['useColumnsForDefaultValues'])) {
457
                if (!$isPagesTable && $calcPerms & Permission::CONTENT_EDIT || $isPagesTable && $calcPerms & Permission::PAGE_NEW) {
458
                    $onClick = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ',' . GeneralUtility::quoteJSvalue($rec['uid']) . ')';
459
                    $style = '';
460
                    if ($inlineConfig['inline']['inlineNewButtonStyle']) {
461
                        $style = ' style="' . $inlineConfig['inline']['inlineNewButtonStyle'] . '"';
462
                    }
463
                    $cells['new'] = '
464
                        <a class="btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'] . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL(('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:new' . ($isPagesTable ? 'Page' : 'Record')))) . '" ' . $style . '>
465
                            ' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page-new' : 'add'), Icon::SIZE_SMALL)->render() . '
466
                        </a>';
467
                }
468
            }
469
            // "Up/Down" links
470
            if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) {
471
                // Up
472
                $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'1\')';
473
                $icon = 'actions-move-up';
474
                $class = '';
475 View Code Duplication
                if ($inlineConfig['inline']['first'] == $rec['uid']) {
476
                    $class = ' disabled';
477
                    $icon = 'empty-empty';
478
                }
479
                $cells['sort.up'] = '
480
                    <a class="btn btn-default sortingUp' . $class . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:moveUp')) . '">
481
                        ' . $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() . '
482
                    </a>';
483
                // Down
484
                $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'-1\')';
485
                $icon = 'actions-move-down';
486
                $class = '';
487 View Code Duplication
                if ($inlineConfig['inline']['last'] == $rec['uid']) {
488
                    $class = ' disabled';
489
                    $icon = 'empty-empty';
490
                }
491
492
                $cells['sort.down'] = '
493
                    <a class="btn btn-default sortingDown' . $class . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:moveDown')) . '">
494
                        ' . $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() . '
495
                    </a>';
496
            }
497
            // "Edit" link:
498
            if (($rec['table_local'] === 'sys_file') && !$isNewItem && $backendUser->check('tables_modify', 'sys_file_metadata')) {
499
                $sys_language_uid = 0;
500
                if (!empty($rec['sys_language_uid'])) {
501
                    $sys_language_uid = $rec['sys_language_uid'][0];
502
                }
503
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
504
                    ->getQueryBuilderForTable('sys_file_metadata');
505
                $recordInDatabase = $queryBuilder
506
                    ->select('uid')
507
                    ->from('sys_file_metadata')
508
                    ->where(
509
                        $queryBuilder->expr()->eq(
510
                            'file',
511
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
512
                        ),
513
                        $queryBuilder->expr()->eq(
514
                            'sys_language_uid',
515
                            $queryBuilder->createNamedParameter($sys_language_uid, \PDO::PARAM_INT)
516
                        )
517
                    )
518
                    ->setMaxResults(1)
519
                    ->execute()
520
                    ->fetch();
521
                if (!empty($recordInDatabase)) {
522
                    $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
523
                    $url = (string)$uriBuilder->buildUriFromRoute('record_edit', [
524
                        'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit',
525
                        'returnUrl' => $this->data['returnUrl']
526
                    ]);
527
                    $title = $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.editMetadata');
528
                    $cells['edit'] = '
529
                        <a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($title) . '">
530
                            ' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '
531
                        </a>';
532
                }
533
            }
534
            // "Delete" link:
535
            if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & Permission::PAGE_DELETE
536
                    || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT
537
                    || $isSysFileReferenceTable && $calcPerms & Permission::PAGE_EDIT)
538
            ) {
539
                $title = htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:delete'));
540
                $icon = $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render();
541
                $cells['delete'] = '<a href="#" class="btn btn-default t3js-editform-delete-inline-record" data-objectid="' . htmlspecialchars($nameObjectFtId) . '" title="' . $title . '">' . $icon . '</a>';
542
            }
543
544
            // "Hide/Unhide" links:
545
            $hiddenField = $tcaTableCtrl['enablecolumns']['disabled'];
546
            if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))) {
547
                $onClick = 'return inline.enableDisableRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ',' .
548
                    GeneralUtility::quoteJSvalue($hiddenField) . ')';
549
                $className = 't3js-' . $nameObjectFtId . '_disabled';
550
                if ($rec[$hiddenField]) {
551
                    $title = htmlspecialchars($languageService->sL(('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : ''))));
552
                    $cells['hide'] = '
553
                        <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="
554
                            ' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">
555
                            ' . $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '
556
                        </a>';
557
                } else {
558
                    $title = htmlspecialchars($languageService->sL(('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : ''))));
559
                    $cells['hide'] = '
560
                        <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="
561
                            ' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">
562
                            ' . $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '
563
                        </a>';
564
                }
565
            }
566
            // Drag&Drop Sorting: Sortable handler for script.aculo.us
567
            if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $inlineConfig['appearance']['useSortable']) {
568
                $cells['dragdrop'] = '
569
                    <span class="btn btn-default sortableHandle" data-id="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move')) . '">
570
                        ' . $this->iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '
571
                    </span>';
572
            }
573
        } elseif ($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] && $isParentExisting) {
574
            if ($enabledControls['localize'] && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
575
                $onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
576
                $cells['localize'] = '
577
                    <a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:localize')) . '">
578
                        ' . $this->iconFactory->getIcon('actions-document-localize', Icon::SIZE_SMALL)->render() . '
579
                    </a>';
580
            }
581
        }
582
        // If the record is edit-locked by another user, we will show a little warning sign:
583
        if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) {
584
            $cells['locked'] = '
585
				<a class="btn btn-default" href="#" data-toggle="tooltip" data-title="' . htmlspecialchars($lockInfo['msg']) . '">
586
					' . '<span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</span>' . '
587
				</a>';
588
        }
589
        // Hook: Post-processing of single controls for specific child records:
590
        foreach ($this->hookObjects as $hookObj) {
591
            $hookObj->renderForeignRecordHeaderControl_postProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $cells);
592
        }
593
594
        $out = '';
595
        if (!empty($cells['edit']) || !empty($cells['hide']) || !empty($cells['delete'])) {
596
            $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['edit'] . $cells['hide'] . $cells['delete'] . '</div>';
597
            unset($cells['edit'], $cells['hide'], $cells['delete']);
598
        }
599
        if (!empty($cells['info']) || !empty($cells['new']) || !empty($cells['sort.up']) || !empty($cells['sort.down']) || !empty($cells['dragdrop'])) {
600
            $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['info'] . $cells['new'] . $cells['sort.up'] . $cells['sort.down'] . $cells['dragdrop'] . '</div>';
601
            unset($cells['info'], $cells['new'], $cells['sort.up'], $cells['sort.down'], $cells['dragdrop']);
602
        }
603
        if (!empty($cells['localize'])) {
604
            $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['localize'] . '</div>';
605
            unset($cells['localize']);
606
        }
607
        if (!empty($cells)) {
608
            $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
609
        }
610
        return $out;
611
    }
612
613
    /**
614
     * Normalize a relation "uid" published by transferData, like "1|Company%201"
615
     *
616
     * @param string $string A transferData reference string, containing the uid
617
     * @return string The normalized uid
618
     */
619
    protected function normalizeUid($string)
620
    {
621
        $parts = explode('|', $string);
622
        return $parts[0];
623
    }
624
625
    /**
626
     * Initialized the hook objects for this class.
627
     * Each hook object has to implement the interface
628
     * \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
629
     *
630
     * @throws \UnexpectedValueException
631
     */
632
    protected function initHookObjects()
633
    {
634
        $this->hookObjects = [];
635
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'] ?? [] as $className) {
636
            $processObject = GeneralUtility::makeInstance($className);
637
            if (!$processObject instanceof InlineElementHookInterface) {
638
                throw new \UnexpectedValueException($className . ' must implement interface ' . InlineElementHookInterface::class, 1202072000);
639
            }
640
            $this->hookObjects[] = $processObject;
641
        }
642
    }
643
644
    /**
645
     * @return BackendUserAuthentication
646
     */
647
    protected function getBackendUserAuthentication()
648
    {
649
        return $GLOBALS['BE_USER'];
650
    }
651
652
    /**
653
     * @return LanguageService
654
     */
655
    protected function getLanguageService()
656
    {
657
        return $GLOBALS['LANG'];
658
    }
659
}
660