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