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