‪TYPO3CMS  10.4
InlineRecordContainer.php
Go to the documentation of this file.
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 
17 
35 
47 {
53  protected ‪$inlineData = [];
54 
58  protected ‪$inlineStackProcessor;
59 
65  protected ‪$hookObjects = [];
66 
70  protected ‪$iconFactory;
71 
79  {
80  parent::__construct(‪$nodeFactory, ‪$data);
81  $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
82  $this->inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
83  $this->‪initHookObjects();
84  }
85 
92  public function ‪render()
93  {
95  $this->inlineData = ‪$data['inlineData'];
96 
99 
100  $record = ‪$data['databaseRow'];
101  $inlineConfig = ‪$data['inlineParentConfig'];
102  $foreignTable = $inlineConfig['foreign_table'];
103 
104  $resultArray = $this->‪initializeResultArray();
105 
106  // Send a mapping information to the browser via JSON:
107  // e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
110  $this->inlineData['map'][$formPrefix] = $domObjectId;
111 
112  $resultArray['inlineData'] = ‪$this->inlineData;
113 
114  // If there is a selector field, normalize it:
115  if (!empty($inlineConfig['foreign_selector'])) {
116  $foreign_selector = $inlineConfig['foreign_selector'];
117  $valueToNormalize = $record[$foreign_selector];
118  if (is_array($record[$foreign_selector])) {
119  // @todo: this can be kicked again if always prepared rows are handled here
120  $valueToNormalize = implode(',', $record[$foreign_selector]);
121  }
122  $record[$foreign_selector] = $this->‪normalizeUid($valueToNormalize);
123  }
124 
125  // Get the current naming scheme for DOM name/id attributes:
126  $appendFormFieldNames = '[' . $foreignTable . '][' . $record['uid'] . ']';
127  $objectId = $domObjectId . '-' . $foreignTable . '-' . $record['uid'];
128  $classes = [];
129  $html = '';
130  $combinationHtml = '';
131  $isNewRecord = ‪$data['command'] === 'new';
132  $hiddenField = '';
133  if (isset(‪$data['processedTca']['ctrl']['enablecolumns']['disabled'])) {
134  $hiddenField = ‪$data['processedTca']['ctrl']['enablecolumns']['disabled'];
135  }
136  if (!‪$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
137  if ($isNewRecord || ‪$data['isInlineChildExpanded']) {
138  // Render full content ONLY IF this is an AJAX request, a new record, or the record is not collapsed
139  $combinationHtml = '';
140  if (isset(‪$data['combinationChild'])) {
141  $combinationChild = $this->‪renderCombinationChild(‪$data, $appendFormFieldNames);
142  $combinationHtml = $combinationChild['html'];
143  $resultArray = $this->‪mergeChildReturnIntoExistingResult($resultArray, $combinationChild, false);
144  }
145  $childArray = $this->‪renderChild(‪$data);
146  $html = $childArray['html'];
147  $resultArray = $this->‪mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
148  } else {
149  // This class is the marker for the JS-function to check if the full content has already been loaded
150  $classes[] = 't3js-not-loaded';
151  }
152  if ($isNewRecord) {
153  // Add pid of record as hidden field
154  $html .= '<input type="hidden" name="data' . htmlspecialchars($appendFormFieldNames)
155  . '[pid]" value="' . htmlspecialchars($record['pid']) . '"/>';
156  // Tell DataHandler this record is expanded
157  $ucFieldName = 'uc[inlineView]'
158  . '[' . ‪$data['inlineTopMostParentTableName'] . ']'
159  . '[' . ‪$data['inlineTopMostParentUid'] . ']'
160  . $appendFormFieldNames;
161  $html .= '<input type="hidden" name="' . htmlspecialchars($ucFieldName)
162  . '" value="' . (int)‪$data['isInlineChildExpanded'] . '" />';
163  } else {
164  // Set additional field for processing for saving
165  $html .= '<input type="hidden" name="cmd' . htmlspecialchars($appendFormFieldNames)
166  . '[delete]" value="1" disabled="disabled" />';
167  if (!‪$data['isInlineChildExpanded'] && !empty($hiddenField)) {
168  $checked = !empty($record[$hiddenField]) ? ' checked="checked"' : '';
169  $html .= '<input type="checkbox" data-formengine-input-name="data'
170  . htmlspecialchars($appendFormFieldNames)
171  . '[' . htmlspecialchars($hiddenField) . ']" value="1"' . $checked . ' />';
172  $html .= '<input type="input" name="data' . htmlspecialchars($appendFormFieldNames)
173  . '[' . htmlspecialchars($hiddenField) . ']" value="' . htmlspecialchars($record[$hiddenField]) . '" />';
174  }
175  }
176  // If this record should be shown collapsed
177  $classes[] = ‪$data['isInlineChildExpanded'] ? 'panel-visible' : 'panel-collapsed';
178  }
179  $hiddenFieldHtml = implode(LF, $resultArray['additionalHiddenFields'] ?? []);
180 
181  if ($inlineConfig['renderFieldsOnly']) {
182  // Render "body" part only
183  $html .= $combinationHtml;
184  } else {
185  // Render header row and content (if expanded)
186  if (‪$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
187  $classes[] = 't3-form-field-container-inline-placeHolder';
188  }
189  if (!empty($hiddenField) && isset($record[$hiddenField]) && (int)$record[$hiddenField]) {
190  $classes[] = 't3-form-field-container-inline-hidden';
191  }
192  if ($isNewRecord) {
193  $classes[] = 'inlineIsNewRecord';
194  }
195 
196  $originalUniqueValue = '';
197  if (isset(‪$data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']])) {
198  $uniqueValueValues = ‪$data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']];
199  // in case of site_language we don't have the full form engine options, so fallbacks need to be taken into account
200  $originalUniqueValue = ($uniqueValueValues['table'] ?? $foreignTable) . '_' . ($uniqueValueValues['uid'] ?? $uniqueValueValues);
201  }
202 
203  // The hashed object id needs a non-numeric prefix, the value is used as ID selector in JavaScript
204  $hashedObjectId = 'hash-' . md5($objectId);
205  $containerAttributes = [
206  'id' => $objectId . '_div',
207  'class' => 'form-irre-object panel panel-default panel-condensed ' . trim(implode(' ', $classes)),
208  'data-object-uid' => $record['uid'],
209  'data-object-id' => $objectId,
210  'data-object-id-hash' => $hashedObjectId,
211  'data-object-parent-group' => $domObjectId . '-' . $foreignTable,
212  'data-field-name' => $appendFormFieldNames,
213  'data-topmost-parent-table' => ‪$data['inlineTopMostParentTableName'],
214  'data-topmost-parent-uid' => ‪$data['inlineTopMostParentUid'],
215  'data-table-unique-original-value' => $originalUniqueValue,
216  'data-placeholder-record' => ‪$data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ? '1' : '0'
217  ];
218 
219  $ariaExpanded = ‪$data['isInlineChildExpanded'] ? 'true' : 'false';
220  $ariaControls = htmlspecialchars($objectId . '_fields', ENT_QUOTES | ENT_HTML5);
221  ‪$data['ariaAttributesString'] = 'aria-expanded="' . $ariaExpanded . '" aria-controls="' . $ariaControls . '"';
222  $html = '
223  <div ' . GeneralUtility::implodeAttributes($containerAttributes, true) . '>
224  <div class="panel-heading" data-toggle="formengine-inline" id="' . htmlspecialchars($hashedObjectId, ENT_QUOTES | ENT_HTML5) . '_header" data-expandSingle="' . ($inlineConfig['appearance']['expandSingle'] ? 1 : 0) . '">
225  <div class="form-irre-header">
226  <div class="form-irre-header-cell form-irre-header-icon">
227  <span class="caret"></span>
228  </div>
229  ' . $this->‪renderForeignRecordHeader(‪$data) . '
230  </div>
231  </div>
232  <div class="panel-collapse" id="' . $ariaControls . '">' . $html . $hiddenFieldHtml . $combinationHtml . '</div>
233  </div>';
234  }
235 
236  $resultArray['html'] = $html;
237  return $resultArray;
238  }
239 
246  protected function ‪renderChild(array ‪$data)
247  {
248  $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix(‪$data['inlineFirstPid']);
249  ‪$data['tabAndInlineStack'][] = [
250  'inline',
251  $domObjectId . '-' . ‪$data['tableName'] . '-' . ‪$data['databaseRow']['uid'],
252  ];
253  // @todo: ugly construct ...
254  ‪$data['inlineData'] = ‪$this->inlineData;
255  ‪$data['renderType'] = 'fullRecordContainer';
256  return $this->nodeFactory->create(‪$data)->render();
257  }
258 
270  protected function ‪renderCombinationChild(array ‪$data, $appendFormFieldNames)
271  {
272  $childData = ‪$data['combinationChild'];
273  $parentConfig = ‪$data['inlineParentConfig'];
274 
275  // If field is set to readOnly, set all fields of the relation to readOnly as well
276  if (isset($parentConfig['readOnly']) && $parentConfig['readOnly']) {
277  foreach ($childData['processedTca']['columns'] as $columnName => $columnConfiguration) {
278  $childData['processedTca']['columns'][$columnName]['config']['readOnly'] = true;
279  }
280  }
281 
282  $resultArray = $this->‪initializeResultArray();
283 
284  // Display Warning FlashMessage if it is not suppressed
285  if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
286  $combinationWarningMessage = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.inline_use_combination';
287  if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
288  $combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
289  }
290  $message = $this->‪getLanguageService()->‪sL($combinationWarningMessage);
291  $markup = [];
292  // @TODO: This is not a FlashMessage! The markup must be changed and special CSS
293  // @TODO: should be created, in order to prevent confusion.
294  $markup[] = '<div class="alert alert-warning">';
295  $markup[] = ' <div class="media">';
296  $markup[] = ' <div class="media-left">';
297  $markup[] = ' <span class="fa-stack fa-lg">';
298  $markup[] = ' <i class="fa fa-circle fa-stack-2x"></i>';
299  $markup[] = ' <i class="fa fa-exclamation fa-stack-1x"></i>';
300  $markup[] = ' </span>';
301  $markup[] = ' </div>';
302  $markup[] = ' <div class="media-body">';
303  $markup[] = ' <div class="alert-message">' . htmlspecialchars($message) . '</div>';
304  $markup[] = ' </div>';
305  $markup[] = ' </div>';
306  $markup[] = '</div>';
307  $resultArray['html'] = implode(LF, $markup);
308  }
309 
310  $childArray = $this->‪renderChild($childData);
311  $resultArray = $this->‪mergeChildReturnIntoExistingResult($resultArray, $childArray);
312 
313  // If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
314  if ($childData['command'] === 'new') {
315  $comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
316  $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
317  }
318  // If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
319  if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
320  $parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
321  $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
322  }
323 
324  return $resultArray;
325  }
326 
334  protected function ‪renderForeignRecordHeader(array ‪$data)
335  {
336  $languageService = $this->‪getLanguageService();
337  $inlineConfig = ‪$data['inlineParentConfig'];
338  $foreignTable = $inlineConfig['foreign_table'];
339  $rec = ‪$data['databaseRow'];
340  // Init:
341  $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix(‪$data['inlineFirstPid']);
342  $objectId = $domObjectId . '-' . $foreignTable . '-' . $rec['uid'];
343 
344  $recordTitle = ‪$data['recordTitle'];
345  if (!empty($recordTitle)) {
346  // The user function may return HTML, therefore we can't escape it
347  if (empty(‪$data['processedTca']['ctrl']['formattedLabel_userFunc'])) {
348  $recordTitle = ‪BackendUtility::getRecordTitlePrep($recordTitle);
349  }
350  } else {
351  $recordTitle = '<em>[' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>';
352  }
353 
354  $altText = ‪BackendUtility::getRecordIconAltText($rec, $foreignTable);
355 
356  $iconImg = '<span title="' . $altText . '" id="' . htmlspecialchars($objectId) . '_icon">' . $this->iconFactory->getIconForRecord($foreignTable, $rec, ‪Icon::SIZE_SMALL)->render() . '</span>';
357  $label = '<span id="' . $objectId . '_label">' . $recordTitle . '</span>';
358  $ctrl = $this->‪renderForeignRecordHeaderControl($data);
359  $thumbnail = false;
360 
361  // Renders a thumbnail for the header
362  if (‪$GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] && !empty($inlineConfig['appearance']['headerThumbnail']['field'])) {
363  $fieldValue = $rec[$inlineConfig['appearance']['headerThumbnail']['field']];
364  $fileUid = $fieldValue[0]['uid'];
365 
366  if (!empty($fileUid)) {
367  try {
368  $fileObject = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($fileUid);
369  } catch (\InvalidArgumentException $e) {
370  $fileObject = null;
371  }
372  if ($fileObject && $fileObject->isMissing()) {
373  $thumbnail .= '<span class="label label-danger">'
374  . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
375  . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
376  } elseif ($fileObject) {
377  $imageSetup = $inlineConfig['appearance']['headerThumbnail'];
378  unset($imageSetup['field']);
379  $cropVariantCollection = ‪CropVariantCollection::create($rec['crop'] ?? '');
380  if (!$cropVariantCollection->getCropArea()->isEmpty()) {
381  $imageSetup['crop'] = $cropVariantCollection->getCropArea()->makeAbsoluteBasedOnFile($fileObject);
382  }
383  $imageSetup = array_merge(['maxWidth' => '145', 'maxHeight' => '45'], $imageSetup);
384 
385  if (‪$GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] && $fileObject->isImage()) {
386  $processedImage = $fileObject->process(‪ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $imageSetup);
387  // Only use a thumbnail if the processing process was successful by checking if image width is set
388  if ($processedImage->getProperty('width')) {
389  $imageUrl = $processedImage->getPublicUrl(true);
390  $thumbnail = '<img src="' . $imageUrl . '" ' .
391  'width="' . $processedImage->getProperty('width') . '" ' .
392  'height="' . $processedImage->getProperty('height') . '" ' .
393  'alt="' . htmlspecialchars($altText) . '" ' .
394  'title="' . htmlspecialchars($altText) . '">';
395  }
396  } else {
397  $thumbnail = '';
398  }
399  }
400  }
401  }
402 
403  if (!empty($inlineConfig['appearance']['headerThumbnail']['field']) && $thumbnail) {
404  $mediaContainer = '<div class="form-irre-header-thumbnail" id="' . $objectId . '_thumbnailcontainer">' . $thumbnail . '</div>';
405  } else {
406  $mediaContainer = '<div class="form-irre-header-icon" id="' . $objectId . '_iconcontainer">' . $iconImg . '</div>';
407  }
408  $header = '<button class="form-irre-header-cell form-irre-header-button" ' . ‪$data['ariaAttributesString'] . '>' .
409  $mediaContainer .
410  '<div class="form-irre-header-body">' . $label . '</div>' .
411  '</button>' .
412  '<div class="form-irre-header-cell form-irre-header-control t3js-formengine-irre-control">' . $ctrl . '</div>';
413 
414  return $header;
415  }
416 
425  protected function ‪renderForeignRecordHeaderControl(array ‪$data)
426  {
427  $rec = ‪$data['databaseRow'];
428  $inlineConfig = ‪$data['inlineParentConfig'];
429  $foreignTable = $inlineConfig['foreign_table'];
430  $languageService = $this->‪getLanguageService();
431  $backendUser = $this->‪getBackendUserAuthentication();
432  // Initialize:
433  $cells = [
434  'edit' => '',
435  'hide' => '',
436  'delete' => '',
437  'info' => '',
438  'new' => '',
439  'sort.up' => '',
440  'sort.down' => '',
441  'dragdrop' => '',
442  'localize' => '',
443  'locked' => '',
444  ];
445  $isNewItem = strpos($rec['uid'], 'NEW') === 0;
446  $isParentReadOnly = isset($inlineConfig['readOnly']) && $inlineConfig['readOnly'];
447  $isParentExisting = ‪MathUtility::canBeInterpretedAsInteger(‪$data['inlineParentUid']);
448  $tcaTableCtrl = ‪$GLOBALS['TCA'][$foreignTable]['ctrl'];
449  $tcaTableCols = ‪$GLOBALS['TCA'][$foreignTable]['columns'];
450  $isPagesTable = $foreignTable === 'pages';
451  $isSysFileReferenceTable = $foreignTable === 'sys_file_reference';
452  $enableManualSorting = $tcaTableCtrl['sortby'] || $inlineConfig['MM'] || !‪$data['isOnSymmetricSide']
453  && $inlineConfig['foreign_sortby'] || ‪$data['isOnSymmetricSide'] && $inlineConfig['symmetric_sortby'];
454  $calcPerms = $backendUser->calcPerms(‪BackendUtility::readPageAccess((int)(‪$data['parentPageRow']['uid'] ?? 0), $backendUser->getPagePermsClause(‪Permission::PAGE_SHOW)));
455  // If the listed table is 'pages' we have to request the permission settings for each page:
456  $localCalcPerms = false;
457  if ($isPagesTable) {
458  $localCalcPerms = $backendUser->calcPerms(‪BackendUtility::getRecord('pages', $rec['uid']));
459  }
460  // This expresses the edit permissions for this particular element:
461  $permsEdit = $isPagesTable && $localCalcPerms & ‪Permission::PAGE_EDIT || !$isPagesTable && $calcPerms & ‪Permission::CONTENT_EDIT;
462  // Controls: Defines which controls should be shown
463  $enabledControls = $inlineConfig['appearance']['enabledControls'];
464  // Hook: Can disable/enable single controls for specific child records:
465  foreach ($this->hookObjects as $hookObj) {
467  $hookObj->renderForeignRecordHeaderControl_preProcess(‪$data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, ‪$data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $enabledControls);
468  }
469  if (‪$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
470  $cells['localize'] = '<span title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize.isLocalizable')) . '">
471  ' . $this->iconFactory->getIcon('actions-edit-localize-status-low', ‪Icon::SIZE_SMALL)->render() . '
472  </span>';
473  }
474  // "Info": (All records)
475  // @todo: hardcoded sys_file!
476  if ($rec['table_local'] === 'sys_file') {
477  $uid = $rec['uid_local'][0]['uid'];
478  $table = '_FILE';
479  } else {
480  $uid = $rec['uid'];
481  $table = $foreignTable;
482  }
483  if ($enabledControls['info']) {
484  if ($isNewItem) {
485  $cells['info'] = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', ‪Icon::SIZE_SMALL)->render() . '</span>';
486  } else {
487  $cells['info'] = '
488  <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')) . '">
489  ' . $this->iconFactory->getIcon('actions-document-info', ‪Icon::SIZE_SMALL)->render() . '
490  </button>';
491  }
492  }
493  // If the table is NOT a read-only table, then show these links:
494  if (!$isParentReadOnly && !$tcaTableCtrl['readOnly'] && !‪$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
495  // "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):
496  if ($enabledControls['new'] && ($enableManualSorting || $tcaTableCtrl['useColumnsForDefaultValues'])) {
497  if (!$isPagesTable && $calcPerms & ‪Permission::CONTENT_EDIT || $isPagesTable && $calcPerms & ‪Permission::PAGE_NEW) {
498  $style = '';
499  if ($inlineConfig['inline']['inlineNewButtonStyle']) {
500  $style = ' style="' . $inlineConfig['inline']['inlineNewButtonStyle'] . '"';
501  }
502  $cells['new'] = '
503  <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 . '>
504  ' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page-new' : 'add'), ‪Icon::SIZE_SMALL)->render() . '
505  </button>';
506  }
507  }
508  // "Up/Down" links
509  if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) {
510  // Up
511  $icon = 'actions-move-up';
512  $class = '';
513  if ($inlineConfig['inline']['first'] == $rec['uid']) {
514  $class = ' disabled';
515  $icon = 'empty-empty';
516  }
517  $cells['sort.up'] = '
518  <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')) . '">
519  ' . $this->iconFactory->getIcon($icon, ‪Icon::SIZE_SMALL)->render() . '
520  </button>';
521  // Down
522  $icon = 'actions-move-down';
523  $class = '';
524  if ($inlineConfig['inline']['last'] == $rec['uid']) {
525  $class = ' disabled';
526  $icon = 'empty-empty';
527  }
528 
529  $cells['sort.down'] = '
530  <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')) . '">
531  ' . $this->iconFactory->getIcon($icon, ‪Icon::SIZE_SMALL)->render() . '
532  </button>';
533  }
534  // "Edit" link:
535  if (($rec['table_local'] === 'sys_file') && !$isNewItem && $backendUser->check('tables_modify', 'sys_file_metadata')) {
536  $sys_language_uid = 0;
537  if (!empty($rec['sys_language_uid'])) {
538  $sys_language_uid = $rec['sys_language_uid'][0];
539  }
540  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
541  ->getQueryBuilderForTable('sys_file_metadata');
542  $recordInDatabase = $queryBuilder
543  ->select('uid')
544  ->from('sys_file_metadata')
545  ->where(
546  $queryBuilder->expr()->eq(
547  'file',
548  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
549  ),
550  $queryBuilder->expr()->eq(
551  'sys_language_uid',
552  $queryBuilder->createNamedParameter($sys_language_uid, \PDO::PARAM_INT)
553  )
554  )
555  ->setMaxResults(1)
556  ->execute()
557  ->fetch();
558  if (!empty($recordInDatabase)) {
559  $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
560  $url = (string)$uriBuilder->buildUriFromRoute('record_edit', [
561  'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit',
562  'returnUrl' => $this->data['returnUrl']
563  ]);
564  $title = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata');
565  $cells['edit'] = '
566  <a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($title) . '">
567  ' . $this->iconFactory->getIcon('actions-open', ‪Icon::SIZE_SMALL)->render() . '
568  </a>';
569  }
570  }
571  // "Delete" link:
572  if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & ‪Permission::PAGE_DELETE
573  || !$isPagesTable && $calcPerms & ‪Permission::CONTENT_EDIT
574  || $isSysFileReferenceTable && $calcPerms & ‪Permission::PAGE_EDIT)
575  ) {
576  $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete'));
577  $icon = $this->iconFactory->getIcon('actions-edit-delete', ‪Icon::SIZE_SMALL)->render();
578  $cells['delete'] = '<button type="button" class="btn btn-default t3js-editform-delete-inline-record" title="' . $title . '">' . $icon . '</button>';
579  }
580 
581  // "Hide/Unhide" links:
582  $hiddenField = $tcaTableCtrl['enablecolumns']['disabled'];
583  if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))) {
584  if ($rec[$hiddenField]) {
585  $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : '')));
586  $cells['hide'] = '
587  <button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . $title . '">
588  ' . $this->iconFactory->getIcon('actions-edit-unhide', ‪Icon::SIZE_SMALL)->render() . '
589  </button>';
590  } else {
591  $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : '')));
592  $cells['hide'] = '
593  <button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . $title . '">
594  ' . $this->iconFactory->getIcon('actions-edit-hide', ‪Icon::SIZE_SMALL)->render() . '
595  </button>';
596  }
597  }
598  // Drag&Drop Sorting: Sortable handler for script.aculo.us
599  if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $inlineConfig['appearance']['useSortable']) {
600  $cells['dragdrop'] = '
601  <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')) . '">
602  ' . $this->iconFactory->getIcon('actions-move-move', ‪Icon::SIZE_SMALL)->render() . '
603  </span>';
604  }
605  } elseif (‪$data['isInlineDefaultLanguageRecordInLocalizedParentContext'] && $isParentExisting) {
606  if ($enabledControls['localize'] && ‪$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
607  $cells['localize'] = '
608  <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')) . '">
609  ' . $this->iconFactory->getIcon('actions-document-localize', ‪Icon::SIZE_SMALL)->render() . '
610  </button>';
611  }
612  }
613  // If the record is edit-locked by another user, we will show a little warning sign:
614  if ($lockInfo = ‪BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) {
615  $cells['locked'] = '
616  <button type="button" class="btn btn-default" data-toggle="tooltip" data-title="' . htmlspecialchars($lockInfo['msg']) . '">
617  <span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $this->iconFactory->getIcon('warning-in-use', ‪Icon::SIZE_SMALL)->render() . '</span>
618  </button>';
619  }
620  // Hook: Post-processing of single controls for specific child records:
621  foreach ($this->hookObjects as $hookObj) {
622  $hookObj->renderForeignRecordHeaderControl_postProcess(‪$data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, ‪$data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $cells);
623  }
624 
625  $out = '';
626  if (!empty($cells['edit']) || !empty($cells['hide']) || !empty($cells['delete'])) {
627  $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['edit'] . $cells['hide'] . $cells['delete'] . '</div>';
628  unset($cells['edit'], $cells['hide'], $cells['delete']);
629  }
630  if (!empty($cells['info']) || !empty($cells['new']) || !empty($cells['sort.up']) || !empty($cells['sort.down']) || !empty($cells['dragdrop'])) {
631  $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['info'] . $cells['new'] . $cells['sort.up'] . $cells['sort.down'] . $cells['dragdrop'] . '</div>';
632  unset($cells['info'], $cells['new'], $cells['sort.up'], $cells['sort.down'], $cells['dragdrop']);
633  }
634  if (!empty($cells['localize'])) {
635  $out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['localize'] . '</div>';
636  unset($cells['localize']);
637  }
638  if (!empty($cells)) {
639  $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
640  }
641  return $out;
642  }
643 
650  protected function ‪normalizeUid($string)
651  {
652  $parts = explode('|', $string);
653  return $parts[0];
654  }
655 
663  protected function ‪initHookObjects()
664  {
665  $this->hookObjects = [];
666  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'] ?? [] as $className) {
667  $processObject = GeneralUtility::makeInstance($className);
668  if (!$processObject instanceof InlineElementHookInterface) {
669  throw new \UnexpectedValueException($className . ' must implement interface ' . InlineElementHookInterface::class, 1202072000);
670  }
671  $this->hookObjects[] = $processObject;
672  }
673  }
674 
678  protected function ‪getBackendUserAuthentication()
679  {
680  return ‪$GLOBALS['BE_USER'];
681  }
682 
686  protected function ‪getLanguageService()
687  {
688  return ‪$GLOBALS['LANG'];
689  }
690 }
‪TYPO3\CMS\Core\Imaging\Icon\SIZE_SMALL
‪const SIZE_SMALL
Definition: Icon.php:30
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\renderForeignRecordHeaderControl
‪string renderForeignRecordHeaderControl(array $data)
Definition: InlineRecordContainer.php:421
‪TYPO3\CMS\Backend\Form\Exception\AccessDeniedContentEditException
Definition: AccessDeniedContentEditException.php:22
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\renderForeignRecordHeader
‪string renderForeignRecordHeader(array $data)
Definition: InlineRecordContainer.php:330
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Backend\Form\AbstractNode\mergeChildReturnIntoExistingResult
‪array mergeChildReturnIntoExistingResult(array $existing, array $childReturn, bool $mergeHtml=true)
Definition: AbstractNode.php:116
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\$inlineStackProcessor
‪InlineStackProcessor $inlineStackProcessor
Definition: InlineRecordContainer.php:56
‪TYPO3\CMS\Backend\Form\AbstractNode\initializeResultArray
‪array initializeResultArray()
Definition: AbstractNode.php:90
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_NEW
‪const PAGE_NEW
Definition: Permission.php:48
‪TYPO3\CMS\Core\Imaging\Icon
Definition: Icon.php:26
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\getLanguageService
‪LanguageService getLanguageService()
Definition: InlineRecordContainer.php:682
‪TYPO3\CMS\Core\Resource\ProcessedFile\CONTEXT_IMAGECROPSCALEMASK
‪const CONTEXT_IMAGECROPSCALEMASK
Definition: ProcessedFile.php:58
‪TYPO3\CMS\Backend\Form\AbstractNode\$nodeFactory
‪NodeFactory $nodeFactory
Definition: AbstractNode.php:36
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\__construct
‪__construct(NodeFactory $nodeFactory, array $data)
Definition: InlineRecordContainer.php:74
‪TYPO3\CMS\Backend\Form\Container
Definition: AbstractContainer.php:16
‪TYPO3\CMS\Backend\Utility\BackendUtility\getRecordIconAltText
‪static string getRecordIconAltText($row, $table='pages')
Definition: BackendUtility.php:1357
‪TYPO3\CMS\Core\Imaging\IconFactory
Definition: IconFactory.php:33
‪TYPO3\CMS\Backend\Form\InlineStackProcessor\getCurrentStructureDomObjectIdPrefix
‪string getCurrentStructureDomObjectIdPrefix($inlineFirstPid)
Definition: InlineStackProcessor.php:164
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\$iconFactory
‪IconFactory $iconFactory
Definition: InlineRecordContainer.php:66
‪TYPO3\CMS\Core\Localization\LanguageService\sL
‪string sL($input)
Definition: LanguageService.php:194
‪TYPO3\CMS\Backend\Form\InlineStackProcessor\initializeByGivenStructure
‪initializeByGivenStructure(array $structure=[])
Definition: InlineStackProcessor.php:42
‪TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
Definition: InlineElementHookInterface.php:22
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:24
‪TYPO3\CMS\Backend\Utility\BackendUtility\getRecordTitlePrep
‪static string getRecordTitlePrep($title, $titleLength=0)
Definition: BackendUtility.php:1614
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\normalizeUid
‪string normalizeUid($string)
Definition: InlineRecordContainer.php:646
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:38
‪TYPO3\CMS\Backend\Form\AbstractNode\$data
‪array $data
Definition: AbstractNode.php:42
‪TYPO3\CMS\Core\Resource\ResourceFactory
Definition: ResourceFactory.php:41
‪TYPO3\CMS\Backend\Form\Container\AbstractContainer
Definition: AbstractContainer.php:28
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:33
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\$inlineData
‪array $inlineData
Definition: InlineRecordContainer.php:52
‪TYPO3\CMS\Backend\Utility\BackendUtility
Definition: BackendUtility.php:75
‪TYPO3\CMS\Backend\Utility\BackendUtility\getRecord
‪static array null getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
Definition: BackendUtility.php:95
‪TYPO3\CMS\Backend\Form\NodeFactory
Definition: NodeFactory.php:37
‪TYPO3\CMS\Core\Resource\ProcessedFile
Definition: ProcessedFile.php:44
‪TYPO3\CMS\Backend\Utility\BackendUtility\readPageAccess
‪static array false readPageAccess($id, $perms_clause)
Definition: BackendUtility.php:597
‪TYPO3\CMS\Backend\Form\InlineStackProcessor\getCurrentStructureFormPrefix
‪string getCurrentStructureFormPrefix()
Definition: InlineStackProcessor.php:147
‪TYPO3\CMS\Core\Type\Bitmask\Permission\CONTENT_EDIT
‪const CONTENT_EDIT
Definition: Permission.php:53
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_EDIT
‪const PAGE_EDIT
Definition: Permission.php:38
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_DELETE
‪const PAGE_DELETE
Definition: Permission.php:43
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer
Definition: InlineRecordContainer.php:47
‪TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection
Definition: CropVariantCollection.php:23
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:22
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\initHookObjects
‪initHookObjects()
Definition: InlineRecordContainer.php:659
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\renderChild
‪array renderChild(array $data)
Definition: InlineRecordContainer.php:242
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:42
‪TYPO3\CMS\Backend\Form\InlineStackProcessor
Definition: InlineStackProcessor.php:30
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\render
‪array render()
Definition: InlineRecordContainer.php:88
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\renderCombinationChild
‪array renderCombinationChild(array $data, $appendFormFieldNames)
Definition: InlineRecordContainer.php:266
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection\create
‪static CropVariantCollection create(string $jsonString, array $tcaConfig=[])
Definition: CropVariantCollection.php:42
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\$hookObjects
‪array $hookObjects
Definition: InlineRecordContainer.php:62
‪TYPO3\CMS\Backend\Form\Container\InlineRecordContainer\getBackendUserAuthentication
‪BackendUserAuthentication getBackendUserAuthentication()
Definition: InlineRecordContainer.php:674
‪TYPO3\CMS\Backend\Utility\BackendUtility\isRecordLocked
‪static array bool isRecordLocked($table, $uid)
Definition: BackendUtility.php:3011