[BUGFIX] FormEngine inline foreign_selector and foreign_unique
[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\Form\Utility\FormEngineUtility;
22 use TYPO3\CMS\Backend\Utility\BackendUtility;
23 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
24 use TYPO3\CMS\Core\Database\DatabaseConnection;
25 use TYPO3\CMS\Core\Database\RelationHandler;
26 use TYPO3\CMS\Core\Imaging\Icon;
27 use TYPO3\CMS\Core\Imaging\IconFactory;
28 use TYPO3\CMS\Core\Messaging\FlashMessage;
29 use TYPO3\CMS\Core\Resource\ProcessedFile;
30 use TYPO3\CMS\Core\Resource\ResourceFactory;
31 use TYPO3\CMS\Core\Type\Bitmask\Permission;
32 use TYPO3\CMS\Core\Utility\GeneralUtility;
33 use TYPO3\CMS\Core\Utility\MathUtility;
34 use TYPO3\CMS\Lang\LanguageService;
35
36 /**
37 * Render a single inline record relation.
38 *
39 * This container is called by InlineControlContainer to render single existing records.
40 * Furthermore it is called by FormEngine for an incoming ajax request to expand an existing record
41 * or to create a new one.
42 *
43 * This container creates the outer HTML of single inline records - eg. drag and drop and delete buttons.
44 * For rendering of the record itself processing is handed over to FullRecordContainer.
45 */
46 class InlineRecordContainer extends AbstractContainer
47 {
48 /**
49 * Inline data array used for JSON output
50 *
51 * @var array
52 */
53 protected $inlineData = array();
54
55 /**
56 * @var InlineStackProcessor
57 */
58 protected $inlineStackProcessor;
59
60 /**
61 * Array containing instances of hook classes called once for IRRE objects
62 *
63 * @var array
64 */
65 protected $hookObjects = array();
66
67 /**
68 * @var IconFactory
69 */
70 protected $iconFactory;
71
72 /**
73 * Default constructor
74 *
75 * @param NodeFactory $nodeFactory
76 * @param array $data
77 */
78 public function __construct(NodeFactory $nodeFactory, array $data)
79 {
80 parent::__construct($nodeFactory, $data);
81 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
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 $this->inlineData = $this->data['inlineData'];
93
94 /** @var InlineStackProcessor $inlineStackProcessor */
95 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
96 $this->inlineStackProcessor = $inlineStackProcessor;
97 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
98
99 $this->initHookObjects();
100
101 $parentUid = $this->data['inlineParentUid'];
102 $record = $this->data['databaseRow'];
103 $config = $this->data['inlineParentConfig'];
104
105 $foreign_table = $config['foreign_table'];
106 $foreign_selector = $config['foreign_selector'];
107 $resultArray = $this->initializeResultArray();
108
109 // Send a mapping information to the browser via JSON:
110 // e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
111 $formPrefix = $inlineStackProcessor->getCurrentStructureFormPrefix();
112 $domObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
113 $this->inlineData['map'][$formPrefix] = $domObjectId;
114
115 $resultArray['inlineData'] = $this->inlineData;
116
117 // Set this variable if we handle a brand new unsaved record:
118 $isNewRecord = !MathUtility::canBeInterpretedAsInteger($record['uid']);
119 // Set this variable if the only the default language record and inline child has not been localized yet
120 $isDefaultLanguageRecord = $this->data['inlineIsDefaultLanguage'];
121 // If there is a selector field, normalize it:
122 if ($foreign_selector) {
123 $valueToNormalize = $record[$foreign_selector];
124 if (is_array($record[$foreign_selector])) {
125 // @todo: this can be kicked again if always prepared rows are handled here
126 $valueToNormalize = implode(',', $record[$foreign_selector]);
127 }
128 $record[$foreign_selector] = $this->normalizeUid($valueToNormalize);
129 }
130 // Get the current naming scheme for DOM name/id attributes:
131 $appendFormFieldNames = '[' . $foreign_table . '][' . $record['uid'] . ']';
132 $objectId = $domObjectId . '-' . $foreign_table . '-' . $record['uid'];
133 $class = '';
134 $html = '';
135 $combinationHtml = '';
136 if (!$isDefaultLanguageRecord) {
137 // Get configuration:
138 $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll'];
139 $expandAll = isset($config['appearance']['collapseAll']) && !$config['appearance']['collapseAll'];
140 $ajaxLoad = !isset($config['appearance']['ajaxLoad']) || $config['appearance']['ajaxLoad'];
141 if ($isNewRecord) {
142 // Show this record expanded or collapsed
143 $isExpanded = $expandAll || (!$collapseAll ? 1 : 0);
144 } else {
145 $isExpanded = $config['renderFieldsOnly'] || !$collapseAll && $this->getExpandedCollapsedState($foreign_table, $record['uid']) || $expandAll;
146 }
147 // Render full content ONLY IF this is an AJAX request, a new record, the record is not collapsed or AJAX loading is explicitly turned off
148 if ($isNewRecord || $isExpanded || !$ajaxLoad) {
149 $combinationHtml = '';
150 if (isset($this->data['combinationChild'])) {
151 $combinationChild = $this->renderCombinationChild($this->data, $appendFormFieldNames);
152 $combinationHtml = $combinationChild['html'];
153 $combinationChild['html'] = '';
154 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild);
155 }
156
157 $childArray = $this->renderChild($this->data);
158
159 $html = $childArray['html'];
160 $childArray['html'] = '';
161 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
162 } else {
163 // This string is the marker for the JS-function to check if the full content has already been loaded
164 $html = '<!--notloaded-->';
165 }
166 if ($isNewRecord) {
167 // Get the top parent table
168 $top = $this->inlineStackProcessor->getStructureLevel(0);
169 $ucFieldName = 'uc[inlineView][' . $top['table'] . '][' . $top['uid'] . ']' . $appendFormFieldNames;
170 // Set additional fields for processing for saving
171 $html .= '<input type="hidden" name="data' . $appendFormFieldNames . '[pid]" value="' . $record['pid'] . '"/>';
172 $html .= '<input type="hidden" name="' . $ucFieldName . '" value="' . $isExpanded . '" />';
173 } else {
174 // Set additional field for processing for saving
175 $html .= '<input type="hidden" name="cmd' . $appendFormFieldNames . '[delete]" value="1" disabled="disabled" />';
176 if (!$isExpanded
177 && !empty($GLOBALS['TCA'][$foreign_table]['ctrl']['enablecolumns']['disabled'])
178 && $ajaxLoad
179 ) {
180 $checked = !empty($record['hidden']) ? ' checked="checked"' : '';
181 $html .= '<input type="checkbox" name="data' . $appendFormFieldNames . '[hidden]_0" value="1"' . $checked . ' />';
182 $html .= '<input type="input" name="data' . $appendFormFieldNames . '[hidden]" value="' . $record['hidden'] . '" />';
183 }
184 }
185 // If this record should be shown collapsed
186 $class = $isExpanded ? 'panel-visible' : 'panel-collapsed';
187 }
188 if ($config['renderFieldsOnly']) {
189 $html = $html . $combinationHtml;
190 } else {
191 // Set the record container with data for output
192 if ($isDefaultLanguageRecord) {
193 $class .= ' t3-form-field-container-inline-placeHolder';
194 }
195 if (isset($record['hidden']) && (int)$record['hidden']) {
196 $class .= ' t3-form-field-container-inline-hidden';
197 }
198 $class .= ($isNewRecord ? ' inlineIsNewRecord' : '');
199 $html = '
200 <div class="panel panel-default panel-condensed ' . trim($class) . '" id="' . $objectId . '_div">
201 <div class="panel-heading" data-toggle="formengine-inline" id="' . $objectId . '_header" data-expandSingle="' . ($config['appearance']['expandSingle'] ? 1 : 0) . '">
202 <div class="form-irre-header">
203 <div class="form-irre-header-cell form-irre-header-icon">
204 <span class="caret"></span>
205 </div>
206 ' . $this->renderForeignRecordHeader($parentUid, $foreign_table, $this->data, $config, $isDefaultLanguageRecord) . '
207 </div>
208 </div>
209 <div class="panel-collapse" id="' . $objectId . '_fields" data-expandSingle="' . ($config['appearance']['expandSingle'] ? 1 : 0) . '" data-returnURL="' . htmlspecialchars(GeneralUtility::getIndpEnv('REQUEST_URI')) . '">' . $html . $combinationHtml . '</div>
210 </div>';
211 }
212
213 $resultArray['html'] = $html;
214 return $resultArray;
215 }
216
217 /**
218 * Render inner child
219 *
220 * @param array $options
221 * @return array Result array
222 */
223 protected function renderChild(array $options)
224 {
225 $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($options['inlineFirstPid']);
226 $options['tabAndInlineStack'][] = [
227 'inline',
228 $domObjectId . '-' . $options['tableName'] . '-' . $options['databaseRow']['uid'],
229 ];
230 // @todo: ugly construct ...
231 $options['inlineData'] = $this->inlineData;
232 $options['renderType'] = 'fullRecordContainer';
233 return $this->nodeFactory->create($options)->render();
234 }
235
236 /**
237 * Render child child
238 *
239 * Render a table with FormEngine, that occurs on an intermediate table but should be editable directly,
240 * so two tables are combined (the intermediate table with attributes and the sub-embedded table).
241 * -> This is a direct embedding over two levels!
242 *
243 * @param array $options
244 * @param string $appendFormFieldNames The [<table>][<uid>] of the parent record (the intermediate table)
245 * @return array Result array
246 */
247 protected function renderCombinationChild(array $options, $appendFormFieldNames)
248 {
249 $childData = $options['combinationChild'];
250 $parentConfig = $options['inlineParentConfig'];
251
252 $resultArray = $this->initializeResultArray();
253
254 // Display Warning FlashMessage if it is not suppressed
255 if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
256 $combinationWarningMessage = 'LLL:EXT:lang/locallang_core.xlf:warning.inline_use_combination';
257 if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
258 $combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
259 }
260 $flashMessage = GeneralUtility::makeInstance(
261 FlashMessage::class,
262 $this->getLanguageService()->sL($combinationWarningMessage),
263 '',
264 FlashMessage::WARNING
265 );
266 $resultArray['html'] = $flashMessage->render();
267 }
268
269 $childArray = $this->renderChild($childData);
270 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
271
272 // If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
273 if ($childData['command'] === 'new') {
274 $comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
275 $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
276 }
277 // If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
278 if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
279 $parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
280 $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
281 }
282
283 return $resultArray;
284 }
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 string $parentUid The uid of the parent (embedding) record (uid or NEW...)
292 * @param string $foreign_table The foreign_table we create a header for
293 * @param array $data Current data
294 * @param array $config content of $PA['fieldConf']['config']
295 * @param bool $isVirtualRecord
296 * @return string The HTML code of the header
297 */
298 protected function renderForeignRecordHeader($parentUid, $foreign_table, $data, $config, $isVirtualRecord = false)
299 {
300 $rec = $data['databaseRow'];
301 // Init:
302 $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
303 $objectId = $domObjectId . '-' . $foreign_table . '-' . $rec['uid'];
304 // We need the returnUrl of the main script when loading the fields via AJAX-call (to correct wizard code, so include it as 3rd parameter)
305 // Pre-Processing:
306 $isOnSymmetricSide = RelationHandler::isOnSymmetricSide($parentUid, $config, $rec);
307 $hasForeignLabel = (bool)(!$isOnSymmetricSide && $config['foreign_label']);
308 $hasSymmetricLabel = (bool)$isOnSymmetricSide && $config['symmetric_label'];
309
310 // Get the record title/label for a record:
311 // Try using a self-defined user function only for formatted labels
312 if (isset($GLOBALS['TCA'][$foreign_table]['ctrl']['formattedLabel_userFunc'])) {
313 $params = array(
314 'table' => $foreign_table,
315 'row' => $rec,
316 'title' => '',
317 'isOnSymmetricSide' => $isOnSymmetricSide,
318 'options' => isset($GLOBALS['TCA'][$foreign_table]['ctrl']['formattedLabel_userFunc_options'])
319 ? $GLOBALS['TCA'][$foreign_table]['ctrl']['formattedLabel_userFunc_options']
320 : array(),
321 'parent' => array(
322 'uid' => $parentUid,
323 'config' => $config
324 )
325 );
326 // callUserFunction requires a third parameter, but we don't want to give $this as reference!
327 $null = null;
328 GeneralUtility::callUserFunction($GLOBALS['TCA'][$foreign_table]['ctrl']['formattedLabel_userFunc'], $params, $null);
329 $recTitle = $params['title'];
330
331 // Try using a normal self-defined user function
332 } elseif (isset($GLOBALS['TCA'][$foreign_table]['ctrl']['label_userFunc'])) {
333 $params = array(
334 'table' => $foreign_table,
335 'row' => $rec,
336 'title' => '',
337 'isOnSymmetricSide' => $isOnSymmetricSide,
338 'parent' => array(
339 'uid' => $parentUid,
340 'config' => $config
341 )
342 );
343 // callUserFunction requires a third parameter, but we don't want to give $this as reference!
344 $null = null;
345 GeneralUtility::callUserFunction($GLOBALS['TCA'][$foreign_table]['ctrl']['label_userFunc'], $params, $null);
346 $recTitle = $params['title'];
347 } elseif ($hasForeignLabel || $hasSymmetricLabel) {
348 $titleCol = $hasForeignLabel ? $config['foreign_label'] : $config['symmetric_label'];
349 // Render title for everything else than group/db:
350 if (isset($this->data['processedTca']['columns'][$titleCol]['config']['type'])
351 && $this->data['processedTca']['columns'][$titleCol]['config']['type'] === 'group'
352 && isset($this->data['processedTca']['columns'][$titleCol]['config']['internal_type'])
353 && $this->data['processedTca']['columns'][$titleCol]['config']['internal_type'] === 'db'
354 ) {
355 $recTitle = BackendUtility::getProcessedValueExtra($foreign_table, $titleCol, $rec[$titleCol], 0, 0, false);
356 } else {
357 // $recTitle could be something like: "tx_table_123|...",
358 $valueParts = GeneralUtility::trimExplode('|', $rec[$titleCol]);
359 $itemParts = GeneralUtility::revExplode('_', $valueParts[0], 2);
360 $recTemp = BackendUtility::getRecordWSOL($itemParts[0], $itemParts[1]);
361 $recTitle = BackendUtility::getRecordTitle($itemParts[0], $recTemp, false);
362 }
363 $recTitle = BackendUtility::getRecordTitlePrep($recTitle);
364 if (trim($recTitle) === '') {
365 $recTitle = BackendUtility::getNoRecordTitle(true);
366 }
367 } else {
368 $recTitle = BackendUtility::getRecordTitle($foreign_table, FormEngineUtility::databaseRowCompatibility($rec), true);
369 }
370
371 $altText = BackendUtility::getRecordIconAltText($rec, $foreign_table);
372
373 $iconImg = '<span title="' . $altText . '" id="' . htmlspecialchars($objectId) . '_icon' . '">' . $this->iconFactory->getIconForRecord($foreign_table, $rec, Icon::SIZE_SMALL)->render() . '</span>';
374 $label = '<span id="' . $objectId . '_label">' . $recTitle . '</span>';
375 $ctrl = $this->renderForeignRecordHeaderControl($parentUid, $foreign_table, $data, $config, $isVirtualRecord);
376 $thumbnail = false;
377
378 // Renders a thumbnail for the header
379 if (!empty($config['appearance']['headerThumbnail']['field'])) {
380 $fieldValue = $rec[$config['appearance']['headerThumbnail']['field']];
381 $firstElement = array_shift(GeneralUtility::trimExplode('|', array_shift(GeneralUtility::trimExplode(',', $fieldValue))));
382 $fileUid = array_pop(BackendUtility::splitTable_Uid($firstElement));
383
384 if (!empty($fileUid)) {
385 $fileObject = ResourceFactory::getInstance()->getFileObject($fileUid);
386 if ($fileObject && $fileObject->isMissing()) {
387 $flashMessage = \TYPO3\CMS\Core\Resource\Utility\BackendUtility::getFlashMessageForMissingFile($fileObject);
388 $thumbnail = $flashMessage->render();
389 } elseif ($fileObject) {
390 $imageSetup = $config['appearance']['headerThumbnail'];
391 unset($imageSetup['field']);
392 if (!empty($rec['crop'])) {
393 $imageSetup['crop'] = $rec['crop'];
394 }
395 $imageSetup = array_merge(array('width' => '45', 'height' => '45c'), $imageSetup);
396 $processedImage = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $imageSetup);
397 // Only use a thumbnail if the processing process was successful by checking if image width is set
398 if ($processedImage->getProperty('width')) {
399 $imageUrl = $processedImage->getPublicUrl(true);
400 $thumbnail = '<img src="' . $imageUrl . '" ' .
401 'width="' . $processedImage->getProperty('width') . '" ' .
402 'height="' . $processedImage->getProperty('height') . '" ' .
403 'alt="' . htmlspecialchars($altText) . '" ' .
404 'title="' . htmlspecialchars($altText) . '">';
405 }
406 }
407 }
408 }
409
410 if (!empty($config['appearance']['headerThumbnail']['field']) && $thumbnail) {
411 $mediaContainer = '<div class="form-irre-header-cell form-irre-header-thumbnail" id="' . $objectId . '_thumbnailcontainer">' . $thumbnail . '</div>';
412 } else {
413 $mediaContainer = '<div class="form-irre-header-cell form-irre-header-icon" id="' . $objectId . '_iconcontainer">' . $iconImg . '</div>';
414 }
415 $header = $mediaContainer . '
416 <div class="form-irre-header-cell form-irre-header-body">' . $label . '</div>
417 <div class="form-irre-header-cell form-irre-header-control t3js-formengine-irre-control">' . $ctrl . '</div>';
418
419 return $header;
420 }
421
422 /**
423 * Render the control-icons for a record header (create new, sorting, delete, disable/enable).
424 * Most of the parts are copy&paste from TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList and
425 * modified for the JavaScript calls here
426 *
427 * @param string $parentUid The uid of the parent (embedding) record (uid or NEW...)
428 * @param string $foreign_table The table (foreign_table) we create control-icons for
429 * @param array $data Current data
430 * @param array $config (modified) TCA configuration of the field
431 * @param bool $isVirtualRecord TRUE if the current record is virtual, FALSE otherwise
432 * @return string The HTML code with the control-icons
433 */
434 protected function renderForeignRecordHeaderControl($parentUid, $foreign_table, $data, $config = array(), $isVirtualRecord = false)
435 {
436 $rec = $data['databaseRow'];
437 $languageService = $this->getLanguageService();
438 $backendUser = $this->getBackendUserAuthentication();
439 // Initialize:
440 $cells = array();
441 $additionalCells = array();
442 $isNewItem = substr($rec['uid'], 0, 3) == 'NEW';
443 $isParentExisting = MathUtility::canBeInterpretedAsInteger($parentUid);
444 $tcaTableCtrl = &$GLOBALS['TCA'][$foreign_table]['ctrl'];
445 $tcaTableCols = &$GLOBALS['TCA'][$foreign_table]['columns'];
446 $isPagesTable = $foreign_table === 'pages';
447 $isSysFileReferenceTable = $foreign_table === 'sys_file_reference';
448 $isOnSymmetricSide = RelationHandler::isOnSymmetricSide($parentUid, $config, $rec);
449 $enableManualSorting = $tcaTableCtrl['sortby'] || $config['MM'] || !$isOnSymmetricSide && $config['foreign_sortby'] || $isOnSymmetricSide && $config['symmetric_sortby'];
450 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
451 $nameObjectFt = $nameObject . '-' . $foreign_table;
452 $nameObjectFtId = $nameObjectFt . '-' . $rec['uid'];
453 $calcPerms = $backendUser->calcPerms(BackendUtility::readPageAccess($rec['pid'], $backendUser->getPagePermsClause(1)));
454 // If the listed table is 'pages' we have to request the permission settings for each page:
455 $localCalcPerms = false;
456 if ($isPagesTable) {
457 $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid']));
458 }
459 // This expresses the edit permissions for this particular element:
460 $permsEdit = $isPagesTable && $localCalcPerms & Permission::PAGE_EDIT || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT;
461 // Controls: Defines which controls should be shown
462 $enabledControls = $config['appearance']['enabledControls'];
463 // Hook: Can disable/enable single controls for specific child records:
464 foreach ($this->hookObjects as $hookObj) {
465 /** @var InlineElementHookInterface $hookObj */
466 $hookObj->renderForeignRecordHeaderControl_preProcess($parentUid, $foreign_table, $rec, $config, $isVirtualRecord, $enabledControls);
467 }
468 if ($data['inlineIsDefaultLanguage']) {
469 $cells['localize.isLocalizable'] = '<span title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.isLocalizable', true) . '">'
470 . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render()
471 . '</span>';
472 } elseif ($data['inlineIsDanglingLocalization']) {
473 $cells['localize.wasRemovedInOriginal'] = '<span title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.wasRemovedInOriginal', true) . '">'
474 . $this->iconFactory->getIcon('actions-edit-localize-status-high', Icon::SIZE_SMALL)->render()
475 . '</span>';
476 }
477 // "Info": (All records)
478 if ($enabledControls['info'] && !$isNewItem) {
479 if ($rec['table_local'] === 'sys_file') {
480 $uid = (int)substr($rec['uid_local'], 9);
481 $table = '_FILE';
482 } else {
483 $uid = $rec['uid'];
484 $table = $foreign_table;
485 }
486 $cells['info'] = '
487 <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) . '">
488 ' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '
489 </a>';
490 }
491 // If the table is NOT a read-only table, then show these links:
492 if (!$tcaTableCtrl['readOnly'] && !$isVirtualRecord) {
493 // "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):
494 if ($enabledControls['new'] && ($enableManualSorting || $tcaTableCtrl['useColumnsForDefaultValues'])) {
495 if (!$isPagesTable && $calcPerms & Permission::CONTENT_EDIT || $isPagesTable && $calcPerms & Permission::PAGE_NEW) {
496 $onClick = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ',' . GeneralUtility::quoteJSvalue($rec['uid']) . ')';
497 $style = '';
498 if ($config['inline']['inlineNewButtonStyle']) {
499 $style = ' style="' . $config['inline']['inlineNewButtonStyle'] . '"';
500 }
501 $cells['new'] = '
502 <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 . '>
503 ' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page' : 'document') . '-new', Icon::SIZE_SMALL)->render() . '
504 </a>';
505 }
506 }
507 // "Up/Down" links
508 if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) {
509 // Up
510 $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'1\')';
511 $style = $config['inline']['first'] == $rec['uid'] ? 'style="visibility: hidden;"' : '';
512 $cells['sort.up'] = '
513 <a class="btn btn-default sortingUp" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . ' title="' . $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveUp', true) . '">
514 ' . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '
515 </a>';
516 // Down
517 $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'-1\')';
518 $style = $config['inline']['last'] == $rec['uid'] ? 'style="visibility: hidden;"' : '';
519 $cells['sort.down'] = '
520 <a class="btn btn-default sortingDown" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . ' title="' . $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveDown', true) . '">
521 ' . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '
522 </a>';
523 }
524 // "Edit" link:
525 if (($rec['table_local'] === 'sys_file') && !$isNewItem) {
526 $sys_language_uid = 0;
527 if (!empty($rec['sys_language_uid'])) {
528 $sys_language_uid = $rec['sys_language_uid'][0];
529 }
530 $recordInDatabase = $this->getDatabaseConnection()->exec_SELECTgetSingleRow(
531 'uid',
532 'sys_file_metadata',
533 'file = ' . (int)substr($rec['uid_local'], 9) . ' AND sys_language_uid = ' . $sys_language_uid
534 );
535 if ($backendUser->check('tables_modify', 'sys_file_metadata')) {
536 $url = BackendUtility::getModuleUrl('record_edit', array(
537 'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit'
538 ));
539 $editOnClick = 'if (top.content.list_frame) {' .
540 'top.content.list_frame.location.href=' .
541 GeneralUtility::quoteJSvalue($url . '&returnUrl=') .
542 '+top.rawurlencode(top.content.list_frame.document.location.pathname+top.content.list_frame.document.location.search)' .
543 ';' .
544 '}';
545 $title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.editMetadata');
546 $cells['editmetadata'] = '
547 <a class="btn btn-default" href="#" class="btn" onclick="' . htmlspecialchars($editOnClick) . '" title="' . htmlspecialchars($title) . '">
548 ' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '
549 </a>';
550 }
551 }
552 // "Delete" link:
553 if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & Permission::PAGE_DELETE
554 || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT
555 || $isSysFileReferenceTable && $calcPerms & Permission::PAGE_EDIT)
556 ) {
557 $title = $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:delete', true);
558 $icon = $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render();
559 $cells['delete'] = '<a href="#" class="btn btn-default t3js-editform-delete-inline-record" data-objectid="' . htmlspecialchars($nameObjectFtId) . '" title="' . $title . '">' . $icon . '</a>';
560 }
561
562 // "Hide/Unhide" links:
563 $hiddenField = $tcaTableCtrl['enablecolumns']['disabled'];
564 if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreign_table . ':' . $hiddenField))) {
565 $onClick = 'return inline.enableDisableRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ')';
566 $className = 't3js-' . $nameObjectFtId . '_disabled';
567 if ($rec[$hiddenField]) {
568 $title = $languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : '')), true);
569 $cells['hide.unhide'] = '
570 <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="'
571 . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">' .
572 $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '
573 </a>';
574 } else {
575 $title = $languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : '')), true);
576 $cells['hide.hide'] = '
577 <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="'
578 . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">' .
579 $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '
580 </a>';
581 }
582 }
583 // Drag&Drop Sorting: Sortable handler for script.aculo.us
584 if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $config['appearance']['useSortable']) {
585 $additionalCells['dragdrop'] = '
586 <span class="btn btn-default sortableHandle" data-id="' . htmlspecialchars($rec['uid']) . '" title="' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.move', true) . '">
587 ' . $this->iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '
588 </span>';
589 }
590 } elseif ($isVirtualRecord && $isParentExisting) {
591 if ($enabledControls['localize'] && $data['inlineIsDefaultLanguage']) {
592 $onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
593 $cells['localize'] = '
594 <a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . 'title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize', true) . '">
595 ' . $this->iconFactory->getIcon('actions-document-localize', Icon::SIZE_SMALL)->render() . '
596 </a>';
597 }
598 }
599 // If the record is edit-locked by another user, we will show a little warning sign:
600 if ($lockInfo = BackendUtility::isRecordLocked($foreign_table, $rec['uid'])) {
601 $cells['locked'] = '
602 <a class="btn btn-default" href="#" onclick="alert(' . GeneralUtility::quoteJSvalue($lockInfo['msg']) . ');return false;">
603 ' . '<span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $this->iconFactory->getIcon('status-warning-in-use', Icon::SIZE_SMALL)->render() . '</span>' . '
604 </a>';
605 }
606 // Hook: Post-processing of single controls for specific child records:
607 foreach ($this->hookObjects as $hookObj) {
608 $hookObj->renderForeignRecordHeaderControl_postProcess($parentUid, $foreign_table, $rec, $config, $isVirtualRecord, $cells);
609 }
610
611 $out = '';
612 if (!empty($cells)) {
613 $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
614 }
615 if (!empty($additionalCells)) {
616 $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $additionalCells) . '</div>';
617 }
618 return $out;
619 }
620
621 /**
622 * Checks if a uid of a child table is in the inline view settings.
623 *
624 * @param string $table Name of the child table
625 * @param int $uid uid of the the child record
626 * @return bool TRUE=expand, FALSE=collapse
627 */
628 protected function getExpandedCollapsedState($table, $uid)
629 {
630 $inlineView = $this->data['inlineExpandCollapseStateArray'];
631 // @todo Add checking/cleaning for unused tables, records, etc. to save space in uc-field
632 if (isset($inlineView[$table]) && is_array($inlineView[$table])) {
633 if (in_array($uid, $inlineView[$table]) !== false) {
634 return true;
635 }
636 }
637 return false;
638 }
639
640 /**
641 * Normalize a relation "uid" published by transferData, like "1|Company%201"
642 *
643 * @param string $string A transferData reference string, containing the uid
644 * @return string The normalized uid
645 */
646 protected function normalizeUid($string)
647 {
648 $parts = explode('|', $string);
649 return $parts[0];
650 }
651
652 /**
653 * Initialized the hook objects for this class.
654 * Each hook object has to implement the interface
655 * \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
656 *
657 * @throws \UnexpectedValueException
658 * @return void
659 */
660 protected function initHookObjects()
661 {
662 $this->hookObjects = array();
663 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'])) {
664 $tceformsInlineHook = &$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'];
665 if (is_array($tceformsInlineHook)) {
666 foreach ($tceformsInlineHook as $classData) {
667 $processObject = GeneralUtility::getUserObj($classData);
668 if (!$processObject instanceof InlineElementHookInterface) {
669 throw new \UnexpectedValueException('$processObject must implement interface ' . InlineElementHookInterface::class, 1202072000);
670 }
671 $this->hookObjects[] = $processObject;
672 }
673 }
674 }
675 }
676
677 /**
678 * @return BackendUserAuthentication
679 */
680 protected function getBackendUserAuthentication()
681 {
682 return $GLOBALS['BE_USER'];
683 }
684
685 /**
686 * @return DatabaseConnection
687 */
688 protected function getDatabaseConnection()
689 {
690 return $GLOBALS['TYPO3_DB'];
691 }
692
693 /**
694 * @return LanguageService
695 */
696 protected function getLanguageService()
697 {
698 return $GLOBALS['LANG'];
699 }
700 }