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