d00ac33f581fd82e7819ff2dfe4db7d5c7ee1dbb
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Container / InlineRecordContainer.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\Container;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Form\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 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 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'))
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) {
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 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 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 $url = BackendUtility::getModuleUrl('record_edit', [
523 'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit',
524 'returnUrl' => $this->data['returnUrl']
525 ]);
526 $title = $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.editMetadata');
527 $cells['edit'] = '
528 <a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($title) . '">
529 ' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '
530 </a>';
531 }
532 }
533 // "Delete" link:
534 if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & Permission::PAGE_DELETE
535 || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT
536 || $isSysFileReferenceTable && $calcPerms & Permission::PAGE_EDIT)
537 ) {
538 $title = htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:delete'));
539 $icon = $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render();
540 $cells['delete'] = '<a href="#" class="btn btn-default t3js-editform-delete-inline-record" data-objectid="' . htmlspecialchars($nameObjectFtId) . '" title="' . $title . '">' . $icon . '</a>';
541 }
542
543 // "Hide/Unhide" links:
544 $hiddenField = $tcaTableCtrl['enablecolumns']['disabled'];
545 if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))) {
546 $onClick = 'return inline.enableDisableRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ',' .
547 GeneralUtility::quoteJSvalue($hiddenField) . ')';
548 $className = 't3js-' . $nameObjectFtId . '_disabled';
549 if ($rec[$hiddenField]) {
550 $title = htmlspecialchars($languageService->sL(('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : ''))));
551 $cells['hide'] = '
552 <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="
553 ' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">
554 ' . $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '
555 </a>';
556 } else {
557 $title = htmlspecialchars($languageService->sL(('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : ''))));
558 $cells['hide'] = '
559 <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="
560 ' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">
561 ' . $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '
562 </a>';
563 }
564 }
565 // Drag&Drop Sorting: Sortable handler for script.aculo.us
566 if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $inlineConfig['appearance']['useSortable']) {
567 $cells['dragdrop'] = '
568 <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')) . '">
569 ' . $this->iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '
570 </span>';
571 }
572 } elseif ($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] && $isParentExisting) {
573 if ($enabledControls['localize'] && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
574 $onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
575 $cells['localize'] = '
576 <a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:localize')) . '">
577 ' . $this->iconFactory->getIcon('actions-document-localize', Icon::SIZE_SMALL)->render() . '
578 </a>';
579 }
580 }
581 // If the record is edit-locked by another user, we will show a little warning sign:
582 if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) {
583 $cells['locked'] = '
584 <a class="btn btn-default" href="#" data-toggle="tooltip" data-title="' . htmlspecialchars($lockInfo['msg']) . '">
585 ' . '<span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</span>' . '
586 </a>';
587 }
588 // Hook: Post-processing of single controls for specific child records:
589 foreach ($this->hookObjects as $hookObj) {
590 $hookObj->renderForeignRecordHeaderControl_postProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $cells);
591 }
592
593 $out = '';
594 if (!empty($cells['edit']) || !empty($cells['hide']) || !empty($cells['delete'])) {
595 $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['edit'] . $cells['hide'] . $cells['delete'] . '</div>';
596 unset($cells['edit'], $cells['hide'], $cells['delete']);
597 }
598 if (!empty($cells['info']) || !empty($cells['new']) || !empty($cells['sort.up']) || !empty($cells['sort.down']) || !empty($cells['dragdrop'])) {
599 $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['info'] . $cells['new'] . $cells['sort.up'] . $cells['sort.down'] . $cells['dragdrop'] . '</div>';
600 unset($cells['info'], $cells['new'], $cells['sort.up'], $cells['sort.down'], $cells['dragdrop']);
601 }
602 if (!empty($cells['localize'])) {
603 $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['localize'] . '</div>';
604 unset($cells['localize']);
605 }
606 if (!empty($cells)) {
607 $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
608 }
609 return $out;
610 }
611
612 /**
613 * Normalize a relation "uid" published by transferData, like "1|Company%201"
614 *
615 * @param string $string A transferData reference string, containing the uid
616 * @return string The normalized uid
617 */
618 protected function normalizeUid($string)
619 {
620 $parts = explode('|', $string);
621 return $parts[0];
622 }
623
624 /**
625 * Initialized the hook objects for this class.
626 * Each hook object has to implement the interface
627 * \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
628 *
629 * @throws \UnexpectedValueException
630 */
631 protected function initHookObjects()
632 {
633 $this->hookObjects = [];
634 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'] ?? [] as $className) {
635 $processObject = GeneralUtility::makeInstance($className);
636 if (!$processObject instanceof InlineElementHookInterface) {
637 throw new \UnexpectedValueException($className . ' must implement interface ' . InlineElementHookInterface::class, 1202072000);
638 }
639 $this->hookObjects[] = $processObject;
640 }
641 }
642
643 /**
644 * @return BackendUserAuthentication
645 */
646 protected function getBackendUserAuthentication()
647 {
648 return $GLOBALS['BE_USER'];
649 }
650
651 /**
652 * @return LanguageService
653 */
654 protected function getLanguageService()
655 {
656 return $GLOBALS['LANG'];
657 }
658 }