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