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