InlineRecordContainer   F
last analyzed

Complexity

Total Complexity 130

Size/Duplication

Total Lines 640
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 130
eloc 369
c 1
b 0
f 0
dl 0
loc 640
rs 2

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A initHookObjects() 0 9 3
F renderForeignRecordHeaderControl() 0 229 75
A renderChild() 0 11 1
A getBackendUserAuthentication() 0 3 1
B renderCombinationChild() 0 55 10
A getLanguageService() 0 3 1
F render() 0 142 22
C renderForeignRecordHeader() 0 81 16

How to fix   Complexity   

Complex Class

Complex classes like InlineRecordContainer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use InlineRecordContainer, and based on these observations, apply Extract Interface, too.

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\Element\InlineElementHookInterface;
19
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedContentEditException;
20
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
21
use TYPO3\CMS\Backend\Form\NodeFactory;
22
use TYPO3\CMS\Backend\Routing\UriBuilder;
23
use TYPO3\CMS\Backend\Utility\BackendUtility;
24
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
25
use TYPO3\CMS\Core\Database\ConnectionPool;
26
use TYPO3\CMS\Core\Imaging\Icon;
27
use TYPO3\CMS\Core\Imaging\IconFactory;
28
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
29
use TYPO3\CMS\Core\Localization\LanguageService;
30
use TYPO3\CMS\Core\Resource\ProcessedFile;
31
use TYPO3\CMS\Core\Resource\ResourceFactory;
32
use TYPO3\CMS\Core\Type\Bitmask\Permission;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
use TYPO3\CMS\Core\Utility\MathUtility;
35
use TYPO3\CMS\Core\Utility\PathUtility;
36
37
/**
38
 * Render a single inline record relation.
39
 *
40
 * This container is called by InlineControlContainer to render single existing records.
41
 * Furthermore it is called by FormEngine for an incoming ajax request to expand an existing record
42
 * or to create a new one.
43
 *
44
 * This container creates the outer HTML of single inline records - eg. drag and drop and delete buttons.
45
 * For rendering of the record itself processing is handed over to FullRecordContainer.
46
 */
47
class InlineRecordContainer extends AbstractContainer
48
{
49
    /**
50
     * Inline data array used for JSON output
51
     *
52
     * @var array
53
     */
54
    protected $inlineData = [];
55
56
    /**
57
     * @var InlineStackProcessor
58
     */
59
    protected $inlineStackProcessor;
60
61
    /**
62
     * Array containing instances of hook classes called once for IRRE objects
63
     *
64
     * @var array
65
     */
66
    protected $hookObjects = [];
67
68
    /**
69
     * @var IconFactory
70
     */
71
    protected $iconFactory;
72
73
    /**
74
     * Default constructor
75
     *
76
     * @param NodeFactory $nodeFactory
77
     * @param array $data
78
     */
79
    public function __construct(NodeFactory $nodeFactory, array $data)
80
    {
81
        parent::__construct($nodeFactory, $data);
82
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
83
        $this->inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
84
        $this->initHookObjects();
85
    }
86
87
    /**
88
     * Entry method
89
     *
90
     * @return array As defined in initializeResultArray() of AbstractNode
91
     * @throws AccessDeniedContentEditException
92
     */
93
    public function render()
94
    {
95
        $data = $this->data;
96
        $this->inlineData = $data['inlineData'];
97
98
        $inlineStackProcessor = $this->inlineStackProcessor;
99
        $inlineStackProcessor->initializeByGivenStructure($data['inlineStructure']);
100
101
        $record = $data['databaseRow'];
102
        $inlineConfig = $data['inlineParentConfig'];
103
        $foreignTable = $inlineConfig['foreign_table'];
104
105
        $resultArray = $this->initializeResultArray();
106
107
        // Send a mapping information to the browser via JSON:
108
        // e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
109
        $formPrefix = $inlineStackProcessor->getCurrentStructureFormPrefix();
110
        $domObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
111
        $this->inlineData['map'][$formPrefix] = $domObjectId;
112
113
        $resultArray['inlineData'] = $this->inlineData;
114
115
        // Get the current naming scheme for DOM name/id attributes:
116
        $appendFormFieldNames = '[' . $foreignTable . '][' . ($record['uid'] ?? 0) . ']';
117
        $objectId = $domObjectId . '-' . $foreignTable . '-' . ($record['uid'] ?? 0);
118
        $classes = [];
119
        $html = '';
120
        $combinationHtml = '';
121
        $isNewRecord = $data['command'] === 'new';
122
        $hiddenField = '';
123
        if (isset($data['processedTca']['ctrl']['enablecolumns']['disabled'])) {
124
            $hiddenField = $data['processedTca']['ctrl']['enablecolumns']['disabled'];
125
        }
126
        if (!$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
127
            if ($isNewRecord || $data['isInlineChildExpanded']) {
128
                // Render full content ONLY IF this is an AJAX request, a new record, or the record is not collapsed
129
                $combinationHtml = '';
130
                if (isset($data['combinationChild'])) {
131
                    $combinationChild = $this->renderCombinationChild($data, $appendFormFieldNames);
132
                    $combinationHtml = $combinationChild['html'];
133
                    $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild, false);
134
                }
135
                $childArray = $this->renderChild($data);
136
                $html = $childArray['html'];
137
                $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
138
            } else {
139
                // This class is the marker for the JS-function to check if the full content has already been loaded
140
                $classes[] = 't3js-not-loaded';
141
            }
142
            if ($isNewRecord) {
143
                // Add pid of record as hidden field
144
                $html .= '<input type="hidden" name="data' . htmlspecialchars($appendFormFieldNames)
145
                    . '[pid]" value="' . htmlspecialchars($record['pid']) . '"/>';
146
                // Tell DataHandler this record is expanded
147
                $ucFieldName = 'uc[inlineView]'
148
                    . '[' . $data['inlineTopMostParentTableName'] . ']'
149
                    . '[' . $data['inlineTopMostParentUid'] . ']'
150
                    . $appendFormFieldNames;
151
                $html .= '<input type="hidden" name="' . htmlspecialchars($ucFieldName)
152
                    . '" value="' . (int)$data['isInlineChildExpanded'] . '" />';
153
            } else {
154
                // Set additional field for processing for saving
155
                $html .= '<input type="hidden" name="cmd' . htmlspecialchars($appendFormFieldNames)
156
                    . '[delete]" value="1" disabled="disabled" />';
157
                if (!$data['isInlineChildExpanded'] && !empty($hiddenField)) {
158
                    $checked = !empty($record[$hiddenField]) ? ' checked="checked"' : '';
159
                    $html .= '<input type="checkbox" data-formengine-input-name="data'
160
                        . htmlspecialchars($appendFormFieldNames)
161
                        . '[' . htmlspecialchars($hiddenField) . ']" value="1"' . $checked . ' />';
162
                    $html .= '<input type="input" name="data' . htmlspecialchars($appendFormFieldNames)
163
                        . '[' . htmlspecialchars($hiddenField) . ']" value="' . htmlspecialchars($record[$hiddenField]) . '" />';
164
                }
165
            }
166
            // If this record should be shown collapsed
167
            $classes[] = $data['isInlineChildExpanded'] ? 'panel-visible' : 'panel-collapsed';
168
        }
169
        $hiddenFieldHtml = implode(LF, $resultArray['additionalHiddenFields'] ?? []);
170
171
        if ($inlineConfig['renderFieldsOnly'] ?? false) {
172
            // Render "body" part only
173
            $html .= $combinationHtml;
174
        } else {
175
            // Render header row and content (if expanded)
176
            if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
177
                $classes[] = 't3-form-field-container-inline-placeHolder';
178
            }
179
            if (!empty($hiddenField) && isset($record[$hiddenField]) && (int)$record[$hiddenField]) {
180
                $classes[] = 't3-form-field-container-inline-hidden';
181
            }
182
            if ($isNewRecord) {
183
                $classes[] = 'inlineIsNewRecord';
184
            }
185
186
            $originalUniqueValue = '';
187
            if (isset($record['uid'], $data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']])) {
188
                $uniqueValueValues = $data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']];
189
                // in case of site_language we don't have the full form engine options, so fallbacks need to be taken into account
190
                $originalUniqueValue = ($uniqueValueValues['table'] ?? $foreignTable) . '_';
191
                // @todo In what circumstance would $uniqueValueValues be an array that lacks a 'uid' key? Unclear, but
192
                // it breaks the string concatenation. This is a hacky workaround for type safety only.
193
                $uVV = ($uniqueValueValues['uid'] ?? $uniqueValueValues);
194
                if (is_array($uVV)) {
195
                    $uVV = implode(',', $uVV);
196
                }
197
                $originalUniqueValue .= $uVV;
198
            }
199
200
            // The hashed object id needs a non-numeric prefix, the value is used as ID selector in JavaScript
201
            $hashedObjectId = 'hash-' . md5($objectId);
202
            $containerAttributes = [
203
                'id' => $objectId . '_div',
204
                'class' => 'form-irre-object panel panel-default panel-condensed ' . trim(implode(' ', $classes)),
205
                'data-object-uid' => $record['uid'] ?? 0,
206
                'data-object-id' => $objectId,
207
                'data-object-id-hash' => $hashedObjectId,
208
                'data-object-parent-group' => $domObjectId . '-' . $foreignTable,
209
                'data-field-name' => $appendFormFieldNames,
210
                'data-topmost-parent-table' => $data['inlineTopMostParentTableName'],
211
                'data-topmost-parent-uid' => $data['inlineTopMostParentUid'],
212
                'data-table-unique-original-value' => $originalUniqueValue,
213
                'data-placeholder-record' => $data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ? '1' : '0'
214
            ];
215
216
            $ariaExpanded = ($data['isInlineChildExpanded'] ?? false) ? 'true' : 'false';
217
            $ariaControls = htmlspecialchars($objectId . '_fields', ENT_QUOTES | ENT_HTML5);
218
            $ariaAttributesString = 'aria-expanded="' . $ariaExpanded . '" aria-controls="' . $ariaControls . '"';
219
            $html = '
220
				<div ' . GeneralUtility::implodeAttributes($containerAttributes, true) . '>
221
					<div class="panel-heading" data-bs-toggle="formengine-inline" id="' . htmlspecialchars($hashedObjectId, ENT_QUOTES | ENT_HTML5) . '_header" data-expandSingle="' . (($inlineConfig['appearance']['expandSingle'] ?? false) ? 1 : 0) . '">
222
						<div class="form-irre-header">
223
							<div class="form-irre-header-cell form-irre-header-icon">
224
								<span class="caret"></span>
225
							</div>
226
							' . $this->renderForeignRecordHeader($data, $ariaAttributesString) . '
227
						</div>
228
					</div>
229
					<div class="panel-collapse" id="' . $ariaControls . '">' . $html . $hiddenFieldHtml . $combinationHtml . '</div>
230
				</div>';
231
        }
232
233
        $resultArray['html'] = $html;
234
        return $resultArray;
235
    }
236
237
    /**
238
     * Render inner child
239
     *
240
     * @param array $data
241
     * @return array Result array
242
     */
243
    protected function renderChild(array $data)
244
    {
245
        $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
246
        $data['tabAndInlineStack'][] = [
247
            'inline',
248
            $domObjectId . '-' . $data['tableName'] . '-' . $data['databaseRow']['uid'],
249
        ];
250
        // @todo: ugly construct ...
251
        $data['inlineData'] = $this->inlineData;
252
        $data['renderType'] = 'fullRecordContainer';
253
        return $this->nodeFactory->create($data)->render();
254
    }
255
256
    /**
257
     * Render child child
258
     *
259
     * Render a table with FormEngine, that occurs on an intermediate table but should be editable directly,
260
     * so two tables are combined (the intermediate table with attributes and the sub-embedded table).
261
     * -> This is a direct embedding over two levels!
262
     *
263
     * @param array $data
264
     * @param string $appendFormFieldNames The [<table>][<uid>] of the parent record (the intermediate table)
265
     * @return array Result array
266
     */
267
    protected function renderCombinationChild(array $data, $appendFormFieldNames)
268
    {
269
        $childData = $data['combinationChild'];
270
        $parentConfig = $data['inlineParentConfig'];
271
272
        // If field is set to readOnly, set all fields of the relation to readOnly as well
273
        if (isset($parentConfig['readOnly']) && $parentConfig['readOnly']) {
274
            foreach ($childData['processedTca']['columns'] as $columnName => $columnConfiguration) {
275
                $childData['processedTca']['columns'][$columnName]['config']['readOnly'] = true;
276
            }
277
        }
278
279
        $resultArray = $this->initializeResultArray();
280
281
        // Display Warning FlashMessage if it is not suppressed
282
        if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
283
            $combinationWarningMessage = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.inline_use_combination';
284
            if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
285
                $combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
286
            }
287
            $message = $this->getLanguageService()->sL($combinationWarningMessage);
288
            $markup = [];
289
            // @TODO: This is not a FlashMessage! The markup must be changed and special CSS
290
            // @TODO: should be created, in order to prevent confusion.
291
            $markup[] = '<div class="alert alert-warning">';
292
            $markup[] = '    <div class="media">';
293
            $markup[] = '        <div class="media-left">';
294
            $markup[] = '            <span class="fa-stack fa-lg">';
295
            $markup[] = '                <i class="fa fa-circle fa-stack-2x"></i>';
296
            $markup[] = '                <i class="fa fa-exclamation fa-stack-1x"></i>';
297
            $markup[] = '            </span>';
298
            $markup[] = '        </div>';
299
            $markup[] = '        <div class="media-body">';
300
            $markup[] = '            <div class="alert-message">' . htmlspecialchars($message) . '</div>';
301
            $markup[] = '        </div>';
302
            $markup[] = '    </div>';
303
            $markup[] = '</div>';
304
            $resultArray['html'] = implode(LF, $markup);
305
        }
306
307
        $childArray = $this->renderChild($childData);
308
        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
309
310
        // If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
311
        if ($childData['command'] === 'new') {
312
            $comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
313
            $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
314
        }
315
        // If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
316
        if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
317
            $parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
318
            $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
319
        }
320
321
        return $resultArray;
322
    }
323
324
    /**
325
     * Renders the HTML header for a foreign record, such as the title, toggle-function, drag'n'drop, etc.
326
     * Later on the command-icons are inserted here.
327
     *
328
     * @param array $data Current data
329
     * @param string $ariaAttributesString HTML aria attributes for the collapse button
330
     * @return string The HTML code of the header
331
     */
332
    protected function renderForeignRecordHeader(array $data, string $ariaAttributesString)
333
    {
334
        $languageService = $this->getLanguageService();
335
        $inlineConfig = $data['inlineParentConfig'];
336
        $foreignTable = $inlineConfig['foreign_table'];
337
        $rec = $data['databaseRow'];
338
        // Init:
339
        $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
340
        $objectId = $domObjectId . '-' . $foreignTable . '-' . ($rec['uid'] ?? 0);
341
342
        $recordTitle = $data['recordTitle'];
343
        if (!empty($recordTitle)) {
344
            // The user function may return HTML, therefore we can't escape it
345
            if (empty($data['processedTca']['ctrl']['formattedLabel_userFunc'])) {
346
                $recordTitle = BackendUtility::getRecordTitlePrep($recordTitle);
347
            }
348
        } else {
349
            $recordTitle = '<em>[' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>';
350
        }
351
352
        $altText = BackendUtility::getRecordIconAltText($rec, $foreignTable);
353
354
        $iconImg = '<span title="' . $altText . '" id="' . htmlspecialchars($objectId) . '_icon">' . $this->iconFactory->getIconForRecord($foreignTable, $rec, Icon::SIZE_SMALL)->render() . '</span>';
355
        $label = '<span id="' . $objectId . '_label">' . $recordTitle . '</span>';
356
        $ctrl = $this->renderForeignRecordHeaderControl($data);
357
        $thumbnail = false;
358
359
        // Renders a thumbnail for the header
360
        if (($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] ?? false) && !empty($inlineConfig['appearance']['headerThumbnail']['field'])) {
361
            $fieldValue = $rec[$inlineConfig['appearance']['headerThumbnail']['field']];
362
            $fileUid = $fieldValue[0]['uid'];
363
364
            if (!empty($fileUid)) {
365
                try {
366
                    $fileObject = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($fileUid);
367
                } catch (\InvalidArgumentException $e) {
368
                    $fileObject = null;
369
                }
370
                if ($fileObject && $fileObject->isMissing()) {
371
                    $thumbnail .= '<span class="label label-danger">'
372
                        . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
373
                        . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
374
                } elseif ($fileObject) {
375
                    $imageSetup = $inlineConfig['appearance']['headerThumbnail'] ?? [];
376
                    unset($imageSetup['field']);
377
                    $cropVariantCollection = CropVariantCollection::create($rec['crop'] ?? '');
378
                    if (!$cropVariantCollection->getCropArea()->isEmpty()) {
379
                        $imageSetup['crop'] = $cropVariantCollection->getCropArea()->makeAbsoluteBasedOnFile($fileObject);
380
                    }
381
                    $imageSetup = array_merge(['maxWidth' => '145', 'maxHeight' => '45'], $imageSetup);
382
383
                    if (($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] ?? false) && $fileObject->isImage()) {
384
                        $processedImage = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $imageSetup);
385
                        // Only use a thumbnail if the processing process was successful by checking if image width is set
386
                        if ($processedImage->getProperty('width')) {
387
                            $imageUrl = $processedImage->getPublicUrl() ?? '';
388
                            $thumbnail = '<img src="' . PathUtility::getAbsoluteWebPath($imageUrl) . '" ' .
389
                                'width="' . $processedImage->getProperty('width') . '" ' .
390
                                'height="' . $processedImage->getProperty('height') . '" ' .
391
                                'alt="' . htmlspecialchars($altText) . '" ' .
392
                                'title="' . htmlspecialchars($altText) . '">';
393
                        }
394
                    } else {
395
                        $thumbnail = '';
396
                    }
397
                }
398
            }
399
        }
400
401
        if (!empty($inlineConfig['appearance']['headerThumbnail']['field']) && $thumbnail) {
402
            $mediaContainer = '<div class="form-irre-header-thumbnail" id="' . $objectId . '_thumbnailcontainer">' . $thumbnail . '</div>';
403
        } else {
404
            $mediaContainer = '<div class="form-irre-header-icon" id="' . $objectId . '_iconcontainer">' . $iconImg . '</div>';
405
        }
406
        $header = '<button class="form-irre-header-cell form-irre-header-button" ' . $ariaAttributesString . '>' .
407
            $mediaContainer .
408
            '<div class="form-irre-header-body">' . $label . '</div>' .
409
            '</button>' .
410
            '<div class="form-irre-header-cell form-irre-header-control t3js-formengine-irre-control">' . $ctrl . '</div>';
411
412
        return $header;
413
    }
414
415
    /**
416
     * Render the control-icons for a record header (create new, sorting, delete, disable/enable).
417
     * Most of the parts are copy&paste from TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList and
418
     * modified for the JavaScript calls here
419
     *
420
     * @param array $data Current data
421
     * @return string The HTML code with the control-icons
422
     */
423
    protected function renderForeignRecordHeaderControl(array $data)
424
    {
425
        $rec = $data['databaseRow'];
426
        $rec += [
427
            'uid' => 0,
428
            'table_local' => '',
429
            'sys_language_uid' => '',
430
        ];
431
        $inlineConfig = $data['inlineParentConfig'];
432
        $foreignTable = $inlineConfig['foreign_table'];
433
        $languageService = $this->getLanguageService();
434
        $backendUser = $this->getBackendUserAuthentication();
435
        // Initialize:
436
        $cells = [
437
            'edit' => '',
438
            'hide' => '',
439
            'delete' => '',
440
            'info' => '',
441
            'new' => '',
442
            'sort.up' => '',
443
            'sort.down' => '',
444
            'dragdrop' => '',
445
            'localize' => '',
446
            'locked' => '',
447
        ];
448
        $isNewItem = strpos($rec['uid'], 'NEW') === 0;
449
        $isParentReadOnly = isset($inlineConfig['readOnly']) && $inlineConfig['readOnly'];
450
        $isParentExisting = MathUtility::canBeInterpretedAsInteger($data['inlineParentUid']);
451
        $tcaTableCtrl = $GLOBALS['TCA'][$foreignTable]['ctrl'];
452
        $tcaTableCols = $GLOBALS['TCA'][$foreignTable]['columns'];
453
        $isPagesTable = $foreignTable === 'pages';
454
        $isSysFileReferenceTable = $foreignTable === 'sys_file_reference';
455
        $enableManualSorting = ($tcaTableCtrl['sortby'] ?? false)
456
            || ($inlineConfig['MM'] ?? false)
457
            || (!($data['isOnSymmetricSide'] ?? false) && ($inlineConfig['foreign_sortby'] ?? false))
458
            || (($data['isOnSymmetricSide'] ?? false) && ($inlineConfig['symmetric_sortby'] ?? false));
459
        $calcPerms = new Permission($backendUser->calcPerms(BackendUtility::readPageAccess((int)($data['parentPageRow']['uid'] ?? 0), $backendUser->getPagePermsClause(Permission::PAGE_SHOW))));
0 ignored issues
show
Bug introduced by
It seems like TYPO3\CMS\Backend\Utilit...Permission::PAGE_SHOW)) can also be of type false; however, parameter $row of TYPO3\CMS\Core\Authentic...entication::calcPerms() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

459
        $calcPerms = new Permission($backendUser->calcPerms(/** @scrutinizer ignore-type */ BackendUtility::readPageAccess((int)($data['parentPageRow']['uid'] ?? 0), $backendUser->getPagePermsClause(Permission::PAGE_SHOW))));
Loading history...
460
        // If the listed table is 'pages' we have to request the permission settings for each page:
461
        $localCalcPerms = new Permission(Permission::NOTHING);
462
        if ($isPagesTable) {
463
            $localCalcPerms = new Permission($backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid'])));
464
        }
465
        // This expresses the edit permissions for this particular element:
466
        $permsEdit = ($isPagesTable && $localCalcPerms->editPagePermissionIsGranted()) || (!$isPagesTable && $calcPerms->editContentPermissionIsGranted());
467
        // Controls: Defines which controls should be shown
468
        $enabledControls = $inlineConfig['appearance']['enabledControls'];
469
        // Hook: Can disable/enable single controls for specific child records:
470
        foreach ($this->hookObjects as $hookObj) {
471
            /** @var InlineElementHookInterface $hookObj */
472
            $hookObj->renderForeignRecordHeaderControl_preProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $enabledControls);
473
        }
474
        if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
475
            $cells['localize'] = '<span title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize.isLocalizable')) . '">
476
                    ' . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render() . '
477
                </span>';
478
        }
479
        // "Info": (All records)
480
        // @todo: hardcoded sys_file!
481
        if ($rec['table_local'] === 'sys_file') {
482
            $uid = $rec['uid_local'][0]['uid'];
483
            $table = '_FILE';
484
        } else {
485
            $uid = $rec['uid'];
486
            $table = $foreignTable;
487
        }
488
        if ($enabledControls['info']) {
489
            if ($isNewItem) {
490
                $cells['info'] = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
491
            } else {
492
                $cells['info'] = '
493
				<button type="button" class="btn btn-default" data-action="infowindow" data-info-table="' . htmlspecialchars($table) . '" data-info-uid="' . htmlspecialchars($uid) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')) . '">
494
					' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '
495
				</button>';
496
            }
497
        }
498
        // If the table is NOT a read-only table, then show these links:
499
        if (!$isParentReadOnly && !($tcaTableCtrl['readOnly'] ?? false) && !($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)) {
500
            // "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):
501
            if (($enabledControls['new'] ?? false) && ($enableManualSorting || ($tcaTableCtrl['useColumnsForDefaultValues'] ?? false))) {
502
                if ((!$isPagesTable && $calcPerms->editContentPermissionIsGranted()) || ($isPagesTable && $calcPerms->createPagePermissionIsGranted())) {
503
                    $style = '';
504
                    if ($inlineConfig['inline']['inlineNewButtonStyle'] ?? false) {
505
                        $style = ' style="' . $inlineConfig['inline']['inlineNewButtonStyle'] . '"';
506
                    }
507
                    $cells['new'] = '
508
                        <button type="button" class="btn btn-default t3js-create-new-button" data-record-uid="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:new' . ($isPagesTable ? 'Page' : 'Record'))) . '" ' . $style . '>
509
                            ' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page-new' : 'add'), Icon::SIZE_SMALL)->render() . '
510
                        </button>';
511
                }
512
            }
513
            // "Up/Down" links
514
            if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) {
515
                // Up
516
                $icon = 'actions-move-up';
517
                $class = '';
518
                if ($inlineConfig['inline']['first'] == $rec['uid']) {
519
                    $class = ' disabled';
520
                    $icon = 'empty-empty';
521
                }
522
                $cells['sort.up'] = '
523
                    <button type="button" class="btn btn-default' . $class . '" data-action="sort" data-direction="up" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveUp')) . '">
524
                        ' . $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() . '
525
                    </button>';
526
                // Down
527
                $icon = 'actions-move-down';
528
                $class = '';
529
                if ($inlineConfig['inline']['last'] == $rec['uid']) {
530
                    $class = ' disabled';
531
                    $icon = 'empty-empty';
532
                }
533
534
                $cells['sort.down'] = '
535
                    <button type="button" class="btn btn-default' . $class . '" data-action="sort" data-direction="down" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveDown')) . '">
536
                        ' . $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() . '
537
                    </button>';
538
            }
539
            // "Edit" link:
540
            if (($rec['table_local'] === 'sys_file') && !$isNewItem && $backendUser->check('tables_modify', 'sys_file_metadata')) {
541
                $sys_language_uid = 0;
542
                if (!empty($rec['sys_language_uid'])) {
543
                    $sys_language_uid = $rec['sys_language_uid'][0];
544
                }
545
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
546
                    ->getQueryBuilderForTable('sys_file_metadata');
547
                $recordInDatabase = $queryBuilder
548
                    ->select('uid')
549
                    ->from('sys_file_metadata')
550
                    ->where(
551
                        $queryBuilder->expr()->eq(
552
                            'file',
553
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
554
                        ),
555
                        $queryBuilder->expr()->eq(
556
                            'sys_language_uid',
557
                            $queryBuilder->createNamedParameter($sys_language_uid, \PDO::PARAM_INT)
558
                        )
559
                    )
560
                    ->setMaxResults(1)
561
                    ->execute()
562
                    ->fetch();
563
                if (!empty($recordInDatabase)) {
564
                    $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
565
                    $url = (string)$uriBuilder->buildUriFromRoute('record_edit', [
566
                        'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit',
567
                        'returnUrl' => $this->data['returnUrl']
568
                    ]);
569
                    $title = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata');
570
                    $cells['edit'] = '
571
                        <a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($title) . '">
572
                            ' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '
573
                        </a>';
574
                }
575
            }
576
            // "Delete" link:
577
            if ($enabledControls['delete'] && (($isPagesTable && $localCalcPerms->deletePagePermissionIsGranted())
578
                    || (!$isPagesTable && $calcPerms->editContentPermissionIsGranted())
579
                    || ($isSysFileReferenceTable && $calcPerms->editPagePermissionIsGranted()))
580
            ) {
581
                $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete'));
582
                $icon = $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render();
583
                $cells['delete'] = '<button type="button" class="btn btn-default t3js-editform-delete-inline-record" title="' . $title . '">' . $icon . '</button>';
584
            }
585
586
            // "Hide/Unhide" links:
587
            $hiddenField = $tcaTableCtrl['enablecolumns']['disabled'] ?? '';
588
            if (($enabledControls['hide'] ?? false)
589
                && $permsEdit
590
                && $hiddenField
591
                && $tcaTableCols[$hiddenField]
592
                && (!($tcaTableCols[$hiddenField]['exclude'] ?? false) || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))
593
            ) {
594
                if ($rec[$hiddenField]) {
595
                    $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : '')));
596
                    $cells['hide'] = '
597
                        <button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . $title . '">
598
                            ' . $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '
599
                        </button>';
600
                } else {
601
                    $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : '')));
602
                    $cells['hide'] = '
603
                        <button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . $title . '">
604
                            ' . $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '
605
                        </button>';
606
                }
607
            }
608
            // Drag&Drop Sorting: Sortable handler for script.aculo.us
609
            if (($enabledControls['dragdrop'] ?? false) && $permsEdit && $enableManualSorting && ($inlineConfig['appearance']['useSortable'] ?? false)) {
610
                $cells['dragdrop'] = '
611
                    <span class="btn btn-default sortableHandle" data-id="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move')) . '">
612
                        ' . $this->iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '
613
                    </span>';
614
            }
615
        } elseif (($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false) && $isParentExisting) {
616
            if (($enabledControls['localize'] ?? false) && ($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)) {
617
                $cells['localize'] = '
618
                    <button type="button" class="btn btn-default t3js-synchronizelocalize-button" data-type="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize')) . '">
619
                        ' . $this->iconFactory->getIcon('actions-document-localize', Icon::SIZE_SMALL)->render() . '
620
                    </button>';
621
            }
622
        }
623
        // If the record is edit-locked by another user, we will show a little warning sign:
624
        if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) {
625
            $cells['locked'] = '
626
				<button type="button" class="btn btn-default" data-bs-toggle="tooltip" title="' . htmlspecialchars($lockInfo['msg']) . '">
627
					' . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '
628
				</button>';
629
        }
630
        // Hook: Post-processing of single controls for specific child records:
631
        foreach ($this->hookObjects as $hookObj) {
632
            $hookObj->renderForeignRecordHeaderControl_postProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $cells);
633
        }
634
635
        $out = '';
636
        if (!empty($cells['edit']) || !empty($cells['hide']) || !empty($cells['delete'])) {
637
            $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['edit'] . $cells['hide'] . $cells['delete'] . '</div>';
638
            unset($cells['edit'], $cells['hide'], $cells['delete']);
639
        }
640
        if (!empty($cells['info']) || !empty($cells['new']) || !empty($cells['sort.up']) || !empty($cells['sort.down']) || !empty($cells['dragdrop'])) {
641
            $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['info'] . $cells['new'] . $cells['sort.up'] . $cells['sort.down'] . $cells['dragdrop'] . '</div>';
642
            unset($cells['info'], $cells['new'], $cells['sort.up'], $cells['sort.down'], $cells['dragdrop']);
643
        }
644
        if (!empty($cells['localize'])) {
645
            $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['localize'] . '</div>';
646
            unset($cells['localize']);
647
        }
648
        if (!empty($cells)) {
649
            $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
650
        }
651
        return $out;
652
    }
653
654
    /**
655
     * Initialized the hook objects for this class.
656
     * Each hook object has to implement the interface
657
     * \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
658
     *
659
     * @throws \UnexpectedValueException
660
     */
661
    protected function initHookObjects()
662
    {
663
        $this->hookObjects = [];
664
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'] ?? [] as $className) {
665
            $processObject = GeneralUtility::makeInstance($className);
666
            if (!$processObject instanceof InlineElementHookInterface) {
667
                throw new \UnexpectedValueException($className . ' must implement interface ' . InlineElementHookInterface::class, 1202072000);
668
            }
669
            $this->hookObjects[] = $processObject;
670
        }
671
    }
672
673
    /**
674
     * @return BackendUserAuthentication
675
     */
676
    protected function getBackendUserAuthentication()
677
    {
678
        return $GLOBALS['BE_USER'];
679
    }
680
681
    /**
682
     * @return LanguageService
683
     */
684
    protected function getLanguageService()
685
    {
686
        return $GLOBALS['LANG'];
687
    }
688
}
689