[TASK] FormEngine: Inline titles and expand / collapse
[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\DatabaseConnection;
24 use TYPO3\CMS\Core\Imaging\Icon;
25 use TYPO3\CMS\Core\Imaging\IconFactory;
26 use TYPO3\CMS\Core\Messaging\FlashMessage;
27 use TYPO3\CMS\Core\Resource\ProcessedFile;
28 use TYPO3\CMS\Core\Resource\ResourceFactory;
29 use TYPO3\CMS\Core\Type\Bitmask\Permission;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\MathUtility;
32 use TYPO3\CMS\Lang\LanguageService;
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 = array();
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 = array();
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 if (!$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
131 $collapseAll = isset($inlineConfig['appearance']['collapseAll']) && $inlineConfig['appearance']['collapseAll'];
132 $expandAll = isset($inlineConfig['appearance']['collapseAll']) && !$inlineConfig['appearance']['collapseAll'];
133 if ($isNewRecord) {
134 $isExpanded = $expandAll || !$collapseAll;
135 } else {
136 $expandCollapseStateArray = $data['inlineExpandCollapseStateArray'];
137 $isExpandedByUcState = isset($expandCollapseStateArray[$foreignTable])
138 && is_array($expandCollapseStateArray[$foreignTable])
139 && in_array($record['uid'], $expandCollapseStateArray[$foreignTable]) !== false;
140 $isExpanded = $inlineConfig['renderFieldsOnly'] || !$collapseAll && $isExpandedByUcState || $expandAll;
141 }
142
143 if ($isNewRecord || $isExpanded) {
144 // Render full content ONLY IF this is an AJAX request, a new record, or the record is not collapsed
145 $combinationHtml = '';
146 if (isset($data['combinationChild'])) {
147 $combinationChild = $this->renderCombinationChild($data, $appendFormFieldNames);
148 $combinationHtml = $combinationChild['html'];
149 $combinationChild['html'] = '';
150 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild);
151 }
152 $childArray = $this->renderChild($data);
153 $html = $childArray['html'];
154 $childArray['html'] = '';
155 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
156 } else {
157 // This string is the marker for the JS-function to check if the full content has already been loaded
158 $html = '<!--notloaded-->';
159 }
160 if ($isNewRecord) {
161 // Add pid of record as hidden field
162 $html .= '<input type="hidden" name="data' . $appendFormFieldNames . '[pid]" value="' . $record['pid'] . '"/>';
163 // Tell DataHandler this record is expanded
164 $ucFieldName = 'uc[inlineView]'
165 . '[' . $data['inlineTopMostParentTableName'] . ']'
166 . '[' . $data['inlineTopMostParentUid'] . ']'
167 . $appendFormFieldNames;
168 $html .= '<input type="hidden" name="' . $ucFieldName . '" value="' . $isExpanded . '" />';
169 } else {
170 // Set additional field for processing for saving
171 $html .= '<input type="hidden" name="cmd' . $appendFormFieldNames . '[delete]" value="1" disabled="disabled" />';
172 if (!$isExpanded
173 && !empty($GLOBALS['TCA'][$foreignTable]['ctrl']['enablecolumns']['disabled'])
174 ) {
175 $checked = !empty($record['hidden']) ? ' checked="checked"' : '';
176 $html .= '<input type="checkbox" name="data' . $appendFormFieldNames . '[hidden]_0" value="1"' . $checked . ' />';
177 $html .= '<input type="input" name="data' . $appendFormFieldNames . '[hidden]" value="' . $record['hidden'] . '" />';
178 }
179 }
180 // If this record should be shown collapsed
181 $class = $isExpanded ? 'panel-visible' : 'panel-collapsed';
182 }
183 if ($inlineConfig['renderFieldsOnly']) {
184 // Render "body" part only
185 $html = $html . $combinationHtml;
186 } else {
187 // Render header row and content (if expanded)
188 if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
189 $class .= ' t3-form-field-container-inline-placeHolder';
190 }
191 if (isset($record['hidden']) && (int)$record['hidden']) {
192 $class .= ' t3-form-field-container-inline-hidden';
193 }
194 $class .= ($isNewRecord ? ' inlineIsNewRecord' : '');
195 $html = '
196 <div class="panel panel-default panel-condensed ' . trim($class) . '" id="' . $objectId . '_div">
197 <div class="panel-heading" data-toggle="formengine-inline" id="' . $objectId . '_header" data-expandSingle="' . ($inlineConfig['appearance']['expandSingle'] ? 1 : 0) . '">
198 <div class="form-irre-header">
199 <div class="form-irre-header-cell form-irre-header-icon">
200 <span class="caret"></span>
201 </div>
202 ' . $this->renderForeignRecordHeader($data) . '
203 </div>
204 </div>
205 <div class="panel-collapse" id="' . $objectId . '_fields" data-expandSingle="' . ($inlineConfig['appearance']['expandSingle'] ? 1 : 0) . '" data-returnURL="' . htmlspecialchars(GeneralUtility::getIndpEnv('REQUEST_URI')) . '">' . $html . $combinationHtml . '</div>
206 </div>';
207 }
208
209 $resultArray['html'] = $html;
210 return $resultArray;
211 }
212
213 /**
214 * Render inner child
215 *
216 * @param array $data
217 * @return array Result array
218 */
219 protected function renderChild(array $data)
220 {
221 $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
222 $data['tabAndInlineStack'][] = [
223 'inline',
224 $domObjectId . '-' . $data['tableName'] . '-' . $data['databaseRow']['uid'],
225 ];
226 // @todo: ugly construct ...
227 $data['inlineData'] = $this->inlineData;
228 $data['renderType'] = 'fullRecordContainer';
229 return $this->nodeFactory->create($data)->render();
230 }
231
232 /**
233 * Render child child
234 *
235 * Render a table with FormEngine, that occurs on an intermediate table but should be editable directly,
236 * so two tables are combined (the intermediate table with attributes and the sub-embedded table).
237 * -> This is a direct embedding over two levels!
238 *
239 * @param array $data
240 * @param string $appendFormFieldNames The [<table>][<uid>] of the parent record (the intermediate table)
241 * @return array Result array
242 */
243 protected function renderCombinationChild(array $data, $appendFormFieldNames)
244 {
245 $childData = $data['combinationChild'];
246 $parentConfig = $data['inlineParentConfig'];
247
248 $resultArray = $this->initializeResultArray();
249
250 // Display Warning FlashMessage if it is not suppressed
251 if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
252 $combinationWarningMessage = 'LLL:EXT:lang/locallang_core.xlf:warning.inline_use_combination';
253 if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
254 $combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
255 }
256 $flashMessage = GeneralUtility::makeInstance(
257 FlashMessage::class,
258 $this->getLanguageService()->sL($combinationWarningMessage),
259 '',
260 FlashMessage::WARNING
261 );
262 $resultArray['html'] = $flashMessage->render();
263 }
264
265 $childArray = $this->renderChild($childData);
266 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
267
268 // If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
269 if ($childData['command'] === 'new') {
270 $comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
271 $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
272 }
273 // If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
274 if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
275 $parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
276 $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
277 }
278
279 return $resultArray;
280 }
281
282
283 /**
284 * Renders the HTML header for a foreign record, such as the title, toggle-function, drag'n'drop, etc.
285 * Later on the command-icons are inserted here.
286 *
287 * @param array $data Current data
288 * @return string The HTML code of the header
289 */
290 protected function renderForeignRecordHeader(array $data)
291 {
292 $languageService = $this->getLanguageService();
293 $inlineConfig = $data['inlineParentConfig'];
294 $foreignTable = $inlineConfig['foreign_table'];
295 $rec = $data['databaseRow'];
296 // Init:
297 $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
298 $objectId = $domObjectId . '-' . $foreignTable . '-' . $rec['uid'];
299
300 $recordTitle = $data['recordTitle'];
301 if (empty($recordTitle)) {
302 $recordTitle = '<em>[' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.no_title', true) . ']</em>';
303 }
304
305 $altText = BackendUtility::getRecordIconAltText($rec, $foreignTable);
306
307 $iconImg = '<span title="' . $altText . '" id="' . htmlspecialchars($objectId) . '_icon' . '">' . $this->iconFactory->getIconForRecord($foreignTable, $rec, Icon::SIZE_SMALL)->render() . '</span>';
308 $label = '<span id="' . $objectId . '_label">' . $recordTitle . '</span>';
309 $ctrl = $this->renderForeignRecordHeaderControl($data);
310 $thumbnail = false;
311
312 // Renders a thumbnail for the header
313 if (!empty($inlineConfig['appearance']['headerThumbnail']['field'])) {
314 $fieldValue = $rec[$inlineConfig['appearance']['headerThumbnail']['field']];
315 $firstElement = array_shift(GeneralUtility::trimExplode('|', array_shift(GeneralUtility::trimExplode(',', $fieldValue))));
316 $fileUid = array_pop(BackendUtility::splitTable_Uid($firstElement));
317
318 if (!empty($fileUid)) {
319 try {
320 $fileObject = ResourceFactory::getInstance()->getFileObject($fileUid);
321 } catch (\InvalidArgumentException $e) {
322 $fileObject = null;
323 }
324 if ($fileObject && $fileObject->isMissing()) {
325 $flashMessage = \TYPO3\CMS\Core\Resource\Utility\BackendUtility::getFlashMessageForMissingFile($fileObject);
326 $thumbnail = $flashMessage->render();
327 } elseif ($fileObject) {
328 $imageSetup = $inlineConfig['appearance']['headerThumbnail'];
329 unset($imageSetup['field']);
330 if (!empty($rec['crop'])) {
331 $imageSetup['crop'] = $rec['crop'];
332 }
333 $imageSetup = array_merge(array('width' => '45', 'height' => '45c'), $imageSetup);
334 $processedImage = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $imageSetup);
335 // Only use a thumbnail if the processing process was successful by checking if image width is set
336 if ($processedImage->getProperty('width')) {
337 $imageUrl = $processedImage->getPublicUrl(true);
338 $thumbnail = '<img src="' . $imageUrl . '" ' .
339 'width="' . $processedImage->getProperty('width') . '" ' .
340 'height="' . $processedImage->getProperty('height') . '" ' .
341 'alt="' . htmlspecialchars($altText) . '" ' .
342 'title="' . htmlspecialchars($altText) . '">';
343 }
344 }
345 }
346 }
347
348 if (!empty($inlineConfig['appearance']['headerThumbnail']['field']) && $thumbnail) {
349 $mediaContainer = '<div class="form-irre-header-cell form-irre-header-thumbnail" id="' . $objectId . '_thumbnailcontainer">' . $thumbnail . '</div>';
350 } else {
351 $mediaContainer = '<div class="form-irre-header-cell form-irre-header-icon" id="' . $objectId . '_iconcontainer">' . $iconImg . '</div>';
352 }
353 $header = $mediaContainer . '
354 <div class="form-irre-header-cell form-irre-header-body">' . $label . '</div>
355 <div class="form-irre-header-cell form-irre-header-control t3js-formengine-irre-control">' . $ctrl . '</div>';
356
357 return $header;
358 }
359
360 /**
361 * Render the control-icons for a record header (create new, sorting, delete, disable/enable).
362 * Most of the parts are copy&paste from TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList and
363 * modified for the JavaScript calls here
364 *
365 * @param array $data Current data
366 * @return string The HTML code with the control-icons
367 */
368 protected function renderForeignRecordHeaderControl(array $data)
369 {
370 $rec = $data['databaseRow'];
371 $inlineConfig = $data['inlineParentConfig'];
372 $foreignTable = $inlineConfig['foreign_table'];
373 $languageService = $this->getLanguageService();
374 $backendUser = $this->getBackendUserAuthentication();
375 // Initialize:
376 $cells = array();
377 $additionalCells = array();
378 $isNewItem = substr($rec['uid'], 0, 3) == 'NEW';
379 $isParentExisting = MathUtility::canBeInterpretedAsInteger($data['inlineParentUid']);
380 $tcaTableCtrl = &$GLOBALS['TCA'][$foreignTable]['ctrl'];
381 $tcaTableCols = &$GLOBALS['TCA'][$foreignTable]['columns'];
382 $isPagesTable = $foreignTable === 'pages';
383 $isSysFileReferenceTable = $foreignTable === 'sys_file_reference';
384 $enableManualSorting = $tcaTableCtrl['sortby'] || $inlineConfig['MM'] || !$data['isOnSymmetricSide']
385 && $inlineConfig['foreign_sortby'] || $data['isOnSymmetricSide'] && $inlineConfig['symmetric_sortby'];
386 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
387 $nameObjectFt = $nameObject . '-' . $foreignTable;
388 $nameObjectFtId = $nameObjectFt . '-' . $rec['uid'];
389 $calcPerms = $backendUser->calcPerms(BackendUtility::readPageAccess($rec['pid'], $backendUser->getPagePermsClause(1)));
390 // If the listed table is 'pages' we have to request the permission settings for each page:
391 $localCalcPerms = false;
392 if ($isPagesTable) {
393 $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid']));
394 }
395 // This expresses the edit permissions for this particular element:
396 $permsEdit = $isPagesTable && $localCalcPerms & Permission::PAGE_EDIT || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT;
397 // Controls: Defines which controls should be shown
398 $enabledControls = $inlineConfig['appearance']['enabledControls'];
399 // Hook: Can disable/enable single controls for specific child records:
400 foreach ($this->hookObjects as $hookObj) {
401 /** @var InlineElementHookInterface $hookObj */
402 $hookObj->renderForeignRecordHeaderControl_preProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $enabledControls);
403 }
404 if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
405 $cells['localize.isLocalizable'] = '<span title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.isLocalizable', true) . '">'
406 . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render()
407 . '</span>';
408 }
409 // "Info": (All records)
410 if ($enabledControls['info'] && !$isNewItem) {
411 if ($rec['table_local'] === 'sys_file') {
412 $uid = (int)substr($rec['uid_local'], 9);
413 $table = '_FILE';
414 } else {
415 $uid = $rec['uid'];
416 $table = $foreignTable;
417 }
418 $cells['info'] = '
419 <a class="btn btn-default" href="#" onclick="' . htmlspecialchars(('top.launchView(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($uid) . '); return false;')) . '" title="' . $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:showInfo', true) . '">
420 ' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '
421 </a>';
422 }
423 // If the table is NOT a read-only table, then show these links:
424 if (!$tcaTableCtrl['readOnly'] && !$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
425 // "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):
426 if ($enabledControls['new'] && ($enableManualSorting || $tcaTableCtrl['useColumnsForDefaultValues'])) {
427 if (!$isPagesTable && $calcPerms & Permission::CONTENT_EDIT || $isPagesTable && $calcPerms & Permission::PAGE_NEW) {
428 $onClick = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ',' . GeneralUtility::quoteJSvalue($rec['uid']) . ')';
429 $style = '';
430 if ($inlineConfig['inline']['inlineNewButtonStyle']) {
431 $style = ' style="' . $inlineConfig['inline']['inlineNewButtonStyle'] . '"';
432 }
433 $cells['new'] = '
434 <a class="btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'] . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:new' . ($isPagesTable ? 'Page' : 'Record')), true) . '" ' . $style . '>
435 ' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page' : 'document') . '-new', Icon::SIZE_SMALL)->render() . '
436 </a>';
437 }
438 }
439 // "Up/Down" links
440 if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) {
441 // Up
442 $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'1\')';
443 $style = $inlineConfig['inline']['first'] == $rec['uid'] ? 'style="visibility: hidden;"' : '';
444 $cells['sort.up'] = '
445 <a class="btn btn-default sortingUp" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . ' title="' . $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveUp', true) . '">
446 ' . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '
447 </a>';
448 // Down
449 $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'-1\')';
450 $style = $inlineConfig['inline']['last'] == $rec['uid'] ? 'style="visibility: hidden;"' : '';
451 $cells['sort.down'] = '
452 <a class="btn btn-default sortingDown" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . ' title="' . $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveDown', true) . '">
453 ' . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '
454 </a>';
455 }
456 // "Edit" link:
457 if (($rec['table_local'] === 'sys_file') && !$isNewItem) {
458 $sys_language_uid = 0;
459 if (!empty($rec['sys_language_uid'])) {
460 $sys_language_uid = $rec['sys_language_uid'][0];
461 }
462 $recordInDatabase = $this->getDatabaseConnection()->exec_SELECTgetSingleRow(
463 'uid',
464 'sys_file_metadata',
465 'file = ' . (int)substr($rec['uid_local'], 9) . ' AND sys_language_uid = ' . $sys_language_uid
466 );
467 if ($backendUser->check('tables_modify', 'sys_file_metadata')) {
468 $url = BackendUtility::getModuleUrl('record_edit', array(
469 'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit'
470 ));
471 $editOnClick = 'if (top.content.list_frame) {' .
472 'top.content.list_frame.location.href=' .
473 GeneralUtility::quoteJSvalue($url . '&returnUrl=') .
474 '+top.rawurlencode(top.content.list_frame.document.location.pathname+top.content.list_frame.document.location.search)' .
475 ';' .
476 '}';
477 $title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.editMetadata');
478 $cells['editmetadata'] = '
479 <a class="btn btn-default" href="#" class="btn" onclick="' . htmlspecialchars($editOnClick) . '" title="' . htmlspecialchars($title) . '">
480 ' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '
481 </a>';
482 }
483 }
484 // "Delete" link:
485 if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & Permission::PAGE_DELETE
486 || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT
487 || $isSysFileReferenceTable && $calcPerms & Permission::PAGE_EDIT)
488 ) {
489 $title = $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:delete', true);
490 $icon = $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render();
491 $cells['delete'] = '<a href="#" class="btn btn-default t3js-editform-delete-inline-record" data-objectid="' . htmlspecialchars($nameObjectFtId) . '" title="' . $title . '">' . $icon . '</a>';
492 }
493
494 // "Hide/Unhide" links:
495 $hiddenField = $tcaTableCtrl['enablecolumns']['disabled'];
496 if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))) {
497 $onClick = 'return inline.enableDisableRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ')';
498 $className = 't3js-' . $nameObjectFtId . '_disabled';
499 if ($rec[$hiddenField]) {
500 $title = $languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : '')), true);
501 $cells['hide.unhide'] = '
502 <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="'
503 . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">' .
504 $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '
505 </a>';
506 } else {
507 $title = $languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : '')), true);
508 $cells['hide.hide'] = '
509 <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="'
510 . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">' .
511 $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '
512 </a>';
513 }
514 }
515 // Drag&Drop Sorting: Sortable handler for script.aculo.us
516 if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $inlineConfig['appearance']['useSortable']) {
517 $additionalCells['dragdrop'] = '
518 <span class="btn btn-default sortableHandle" data-id="' . htmlspecialchars($rec['uid']) . '" title="' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.move', true) . '">
519 ' . $this->iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '
520 </span>';
521 }
522 } elseif ($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] && $isParentExisting) {
523 if ($enabledControls['localize'] && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
524 $onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
525 $cells['localize'] = '
526 <a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize', true) . '">
527 ' . $this->iconFactory->getIcon('actions-document-localize', Icon::SIZE_SMALL)->render() . '
528 </a>';
529 }
530 }
531 // If the record is edit-locked by another user, we will show a little warning sign:
532 if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) {
533 $cells['locked'] = '
534 <a class="btn btn-default" href="#" onclick="alert(' . GeneralUtility::quoteJSvalue($lockInfo['msg']) . ');return false;">
535 ' . '<span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $this->iconFactory->getIcon('status-warning-in-use', Icon::SIZE_SMALL)->render() . '</span>' . '
536 </a>';
537 }
538 // Hook: Post-processing of single controls for specific child records:
539 foreach ($this->hookObjects as $hookObj) {
540 $hookObj->renderForeignRecordHeaderControl_postProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $cells);
541 }
542
543 $out = '';
544 if (!empty($cells)) {
545 $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
546 }
547 if (!empty($additionalCells)) {
548 $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $additionalCells) . '</div>';
549 }
550 return $out;
551 }
552
553 /**
554 * Normalize a relation "uid" published by transferData, like "1|Company%201"
555 *
556 * @param string $string A transferData reference string, containing the uid
557 * @return string The normalized uid
558 */
559 protected function normalizeUid($string)
560 {
561 $parts = explode('|', $string);
562 return $parts[0];
563 }
564
565 /**
566 * Initialized the hook objects for this class.
567 * Each hook object has to implement the interface
568 * \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
569 *
570 * @throws \UnexpectedValueException
571 * @return void
572 */
573 protected function initHookObjects()
574 {
575 $this->hookObjects = array();
576 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'])) {
577 $tceformsInlineHook = &$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'];
578 if (is_array($tceformsInlineHook)) {
579 foreach ($tceformsInlineHook as $classData) {
580 $processObject = GeneralUtility::getUserObj($classData);
581 if (!$processObject instanceof InlineElementHookInterface) {
582 throw new \UnexpectedValueException('$processObject must implement interface ' . InlineElementHookInterface::class, 1202072000);
583 }
584 $this->hookObjects[] = $processObject;
585 }
586 }
587 }
588 }
589
590 /**
591 * @return BackendUserAuthentication
592 */
593 protected function getBackendUserAuthentication()
594 {
595 return $GLOBALS['BE_USER'];
596 }
597
598 /**
599 * @return DatabaseConnection
600 */
601 protected function getDatabaseConnection()
602 {
603 return $GLOBALS['TYPO3_DB'];
604 }
605
606 /**
607 * @return LanguageService
608 */
609 protected function getLanguageService()
610 {
611 return $GLOBALS['LANG'];
612 }
613 }