[BUGFIX] Correct record title escaping
[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\InlineStackProcessor;
20 use TYPO3\CMS\Backend\Form\NodeFactory;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
22 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23 use TYPO3\CMS\Core\Database\ConnectionPool;
24 use TYPO3\CMS\Core\Database\DatabaseConnection;
25 use TYPO3\CMS\Core\Imaging\Icon;
26 use TYPO3\CMS\Core\Imaging\IconFactory;
27 use TYPO3\CMS\Core\Messaging\FlashMessage;
28 use TYPO3\CMS\Core\Resource\ProcessedFile;
29 use TYPO3\CMS\Core\Resource\ResourceFactory;
30 use TYPO3\CMS\Core\Type\Bitmask\Permission;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32 use TYPO3\CMS\Core\Utility\MathUtility;
33 use TYPO3\CMS\Lang\LanguageService;
34
35 /**
36 * Render a single inline record relation.
37 *
38 * This container is called by InlineControlContainer to render single existing records.
39 * Furthermore it is called by FormEngine for an incoming ajax request to expand an existing record
40 * or to create a new one.
41 *
42 * This container creates the outer HTML of single inline records - eg. drag and drop and delete buttons.
43 * For rendering of the record itself processing is handed over to FullRecordContainer.
44 */
45 class InlineRecordContainer extends AbstractContainer
46 {
47 /**
48 * Inline data array used for JSON output
49 *
50 * @var array
51 */
52 protected $inlineData = array();
53
54 /**
55 * @var InlineStackProcessor
56 */
57 protected $inlineStackProcessor;
58
59 /**
60 * Array containing instances of hook classes called once for IRRE objects
61 *
62 * @var array
63 */
64 protected $hookObjects = array();
65
66 /**
67 * @var IconFactory
68 */
69 protected $iconFactory;
70
71 /**
72 * Default constructor
73 *
74 * @param NodeFactory $nodeFactory
75 * @param array $data
76 */
77 public function __construct(NodeFactory $nodeFactory, array $data)
78 {
79 parent::__construct($nodeFactory, $data);
80 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
81 $this->inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
82 $this->initHookObjects();
83 }
84
85 /**
86 * Entry method
87 *
88 * @return array As defined in initializeResultArray() of AbstractNode
89 * @throws AccessDeniedContentEditException
90 */
91 public function render()
92 {
93 $data = $this->data;
94 $this->inlineData = $data['inlineData'];
95
96 $inlineStackProcessor = $this->inlineStackProcessor;
97 $inlineStackProcessor->initializeByGivenStructure($data['inlineStructure']);
98
99 $record = $data['databaseRow'];
100 $inlineConfig = $data['inlineParentConfig'];
101 $foreignTable = $inlineConfig['foreign_table'];
102
103 $resultArray = $this->initializeResultArray();
104
105 // Send a mapping information to the browser via JSON:
106 // e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
107 $formPrefix = $inlineStackProcessor->getCurrentStructureFormPrefix();
108 $domObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
109 $this->inlineData['map'][$formPrefix] = $domObjectId;
110
111 $resultArray['inlineData'] = $this->inlineData;
112
113 // If there is a selector field, normalize it:
114 if (!empty($inlineConfig['foreign_selector'])) {
115 $foreign_selector = $inlineConfig['foreign_selector'];
116 $valueToNormalize = $record[$foreign_selector];
117 if (is_array($record[$foreign_selector])) {
118 // @todo: this can be kicked again if always prepared rows are handled here
119 $valueToNormalize = implode(',', $record[$foreign_selector]);
120 }
121 $record[$foreign_selector] = $this->normalizeUid($valueToNormalize);
122 }
123
124 // Get the current naming scheme for DOM name/id attributes:
125 $appendFormFieldNames = '[' . $foreignTable . '][' . $record['uid'] . ']';
126 $objectId = $domObjectId . '-' . $foreignTable . '-' . $record['uid'];
127 $class = '';
128 $html = '';
129 $combinationHtml = '';
130 $isNewRecord = $data['command'] === 'new';
131 $hiddenField = '';
132 if (isset($data['processedTca']['ctrl']['enablecolumns']['disabled'])) {
133 $hiddenField = $data['processedTca']['ctrl']['enablecolumns']['disabled'];
134 }
135 if (!$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
136 if ($isNewRecord || $data['isInlineChildExpanded']) {
137 // Render full content ONLY IF this is an AJAX request, a new record, or the record is not collapsed
138 $combinationHtml = '';
139 if (isset($data['combinationChild'])) {
140 $combinationChild = $this->renderCombinationChild($data, $appendFormFieldNames);
141 $combinationHtml = $combinationChild['html'];
142 $combinationChild['html'] = '';
143 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild);
144 }
145 $childArray = $this->renderChild($data);
146 $html = $childArray['html'];
147 $childArray['html'] = '';
148 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
149 } else {
150 // This string is the marker for the JS-function to check if the full content has already been loaded
151 $html = '<!--notloaded-->';
152 }
153 if ($isNewRecord) {
154 // Add pid of record as hidden field
155 $html .= '<input type="hidden" name="data' . htmlspecialchars($appendFormFieldNames)
156 . '[pid]" value="' . htmlspecialchars($record['pid']) . '"/>';
157 // Tell DataHandler this record is expanded
158 $ucFieldName = 'uc[inlineView]'
159 . '[' . $data['inlineTopMostParentTableName'] . ']'
160 . '[' . $data['inlineTopMostParentUid'] . ']'
161 . $appendFormFieldNames;
162 $html .= '<input type="hidden" name="' . htmlspecialchars($ucFieldName)
163 . '" value="' . (int)$data['isInlineChildExpanded'] . '" />';
164 } else {
165 // Set additional field for processing for saving
166 $html .= '<input type="hidden" name="cmd' . htmlspecialchars($appendFormFieldNames)
167 . '[delete]" value="1" disabled="disabled" />';
168 if (!$data['isInlineChildExpanded'] && !empty($hiddenField)) {
169 $checked = !empty($record[$hiddenField]) ? ' checked="checked"' : '';
170 $html .= '<input type="checkbox" data-formengine-input-name="data'
171 . htmlspecialchars($appendFormFieldNames)
172 . '[' . htmlspecialchars($hiddenField) . ']" value="1"' . $checked . ' />';
173 $html .= '<input type="input" name="data' . htmlspecialchars($appendFormFieldNames)
174 . '[' . htmlspecialchars($hiddenField) . ']" value="' . htmlspecialchars($record[$hiddenField]) . '" />';
175 }
176 }
177 // If this record should be shown collapsed
178 $class = $data['isInlineChildExpanded'] ? 'panel-visible' : 'panel-collapsed';
179 }
180 if ($inlineConfig['renderFieldsOnly']) {
181 // Render "body" part only
182 $html = $html . $combinationHtml;
183 } else {
184 // Render header row and content (if expanded)
185 if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
186 $class .= ' t3-form-field-container-inline-placeHolder';
187 }
188 if (!empty($hiddenField) && isset($record[$hiddenField]) && (int)$record[$hiddenField]) {
189 $class .= ' t3-form-field-container-inline-hidden';
190 }
191 $class .= ($isNewRecord ? ' inlineIsNewRecord' : '');
192 $html = '
193 <div class="panel panel-default panel-condensed ' . trim($class) . '" id="' . htmlspecialchars($objectId) . '_div">
194 <div class="panel-heading" data-toggle="formengine-inline" id="' . htmlspecialchars($objectId) . '_header" data-expandSingle="' . ($inlineConfig['appearance']['expandSingle'] ? 1 : 0) . '">
195 <div class="form-irre-header">
196 <div class="form-irre-header-cell form-irre-header-icon">
197 <span class="caret"></span>
198 </div>
199 ' . $this->renderForeignRecordHeader($data) . '
200 </div>
201 </div>
202 <div class="panel-collapse" id="' . htmlspecialchars($objectId) . '_fields" data-expandSingle="' . ($inlineConfig['appearance']['expandSingle'] ? 1 : 0) . '" data-returnURL="' . htmlspecialchars(GeneralUtility::getIndpEnv('REQUEST_URI')) . '">' . $html . $combinationHtml . '</div>
203 </div>';
204 }
205
206 $resultArray['html'] = $html;
207 return $resultArray;
208 }
209
210 /**
211 * Render inner child
212 *
213 * @param array $data
214 * @return array Result array
215 */
216 protected function renderChild(array $data)
217 {
218 $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
219 $data['tabAndInlineStack'][] = [
220 'inline',
221 $domObjectId . '-' . $data['tableName'] . '-' . $data['databaseRow']['uid'],
222 ];
223 // @todo: ugly construct ...
224 $data['inlineData'] = $this->inlineData;
225 $data['renderType'] = 'fullRecordContainer';
226 return $this->nodeFactory->create($data)->render();
227 }
228
229 /**
230 * Render child child
231 *
232 * Render a table with FormEngine, that occurs on an intermediate table but should be editable directly,
233 * so two tables are combined (the intermediate table with attributes and the sub-embedded table).
234 * -> This is a direct embedding over two levels!
235 *
236 * @param array $data
237 * @param string $appendFormFieldNames The [<table>][<uid>] of the parent record (the intermediate table)
238 * @return array Result array
239 */
240 protected function renderCombinationChild(array $data, $appendFormFieldNames)
241 {
242 $childData = $data['combinationChild'];
243 $parentConfig = $data['inlineParentConfig'];
244
245 $resultArray = $this->initializeResultArray();
246
247 // Display Warning FlashMessage if it is not suppressed
248 if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
249 $combinationWarningMessage = 'LLL:EXT:lang/locallang_core.xlf:warning.inline_use_combination';
250 if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
251 $combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
252 }
253 $message = $this->getLanguageService()->sL($combinationWarningMessage);
254 $markup = [];
255 // @TODO: This is not a FlashMessage! The markup must be changed and special CSS
256 // @TODO: should be created, in order to prevent confusion.
257 $markup[] = '<div class="alert alert-warning">';
258 $markup[] = ' <div class="media">';
259 $markup[] = ' <div class="media-left">';
260 $markup[] = ' <span class="fa-stack fa-lg">';
261 $markup[] = ' <i class="fa fa-circle fa-stack-2x"></i>';
262 $markup[] = ' <i class="fa fa-exclamation fa-stack-1x"></i>';
263 $markup[] = ' </span>';
264 $markup[] = ' </div>';
265 $markup[] = ' <div class="media-body">';
266 $markup[] = ' <div class="alert-message">' . htmlspecialchars($message) . '</div>';
267 $markup[] = ' </div>';
268 $markup[] = ' </div>';
269 $markup[] = '</div>';
270 $resultArray['html'] = implode(LF, $markup);
271 }
272
273 $childArray = $this->renderChild($childData);
274 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
275
276 // If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
277 if ($childData['command'] === 'new') {
278 $comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
279 $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
280 }
281 // If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
282 if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
283 $parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
284 $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
285 }
286
287 return $resultArray;
288 }
289
290 /**
291 * Renders the HTML header for a foreign record, such as the title, toggle-function, drag'n'drop, etc.
292 * Later on the command-icons are inserted here.
293 *
294 * @param array $data Current data
295 * @return string The HTML code of the header
296 */
297 protected function renderForeignRecordHeader(array $data)
298 {
299 $languageService = $this->getLanguageService();
300 $inlineConfig = $data['inlineParentConfig'];
301 $foreignTable = $inlineConfig['foreign_table'];
302 $rec = $data['databaseRow'];
303 // Init:
304 $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
305 $objectId = $domObjectId . '-' . $foreignTable . '-' . $rec['uid'];
306
307 $recordTitle = $data['recordTitle'];
308 if (!empty($recordTitle)) {
309 $recordTitle = BackendUtility::getRecordTitlePrep($recordTitle);
310 } else {
311 $recordTitle = '<em>[' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.no_title')) . ']</em>';
312 }
313
314 $altText = BackendUtility::getRecordIconAltText($rec, $foreignTable);
315
316 $iconImg = '<span title="' . $altText . '" id="' . htmlspecialchars($objectId) . '_icon' . '">' . $this->iconFactory->getIconForRecord($foreignTable, $rec, Icon::SIZE_SMALL)->render() . '</span>';
317 $label = '<span id="' . $objectId . '_label">' . $recordTitle . '</span>';
318 $ctrl = $this->renderForeignRecordHeaderControl($data);
319 $thumbnail = false;
320
321 // Renders a thumbnail for the header
322 if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] && !empty($inlineConfig['appearance']['headerThumbnail']['field'])) {
323 $fieldValue = $rec[$inlineConfig['appearance']['headerThumbnail']['field']];
324 $firstElement = array_shift(GeneralUtility::trimExplode('|', array_shift(GeneralUtility::trimExplode(',', $fieldValue))));
325 $fileUid = array_pop(BackendUtility::splitTable_Uid($firstElement));
326
327 if (!empty($fileUid)) {
328 try {
329 $fileObject = ResourceFactory::getInstance()->getFileObject($fileUid);
330 } catch (\InvalidArgumentException $e) {
331 $fileObject = null;
332 }
333 if ($fileObject && $fileObject->isMissing()) {
334 $thumbnail .= '<span class="label label-danger">'
335 . htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:warning.file_missing'))
336 . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
337 } elseif ($fileObject) {
338 $imageSetup = $inlineConfig['appearance']['headerThumbnail'];
339 unset($imageSetup['field']);
340 if (!empty($rec['crop'])) {
341 $imageSetup['crop'] = $rec['crop'];
342 }
343 $imageSetup = array_merge(array('width' => '45', 'height' => '45c'), $imageSetup);
344 $processedImage = $fileObject->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, $imageSetup);
345 // Only use a thumbnail if the processing process was successful by checking if image width is set
346 if ($processedImage->getProperty('width')) {
347 $imageUrl = $processedImage->getPublicUrl(true);
348 $thumbnail = '<img src="' . $imageUrl . '" ' .
349 'width="' . $processedImage->getProperty('width') . '" ' .
350 'height="' . $processedImage->getProperty('height') . '" ' .
351 'alt="' . htmlspecialchars($altText) . '" ' .
352 'title="' . htmlspecialchars($altText) . '">';
353 }
354 }
355 }
356 }
357
358 if (!empty($inlineConfig['appearance']['headerThumbnail']['field']) && $thumbnail) {
359 $mediaContainer = '<div class="form-irre-header-cell form-irre-header-thumbnail" id="' . $objectId . '_thumbnailcontainer">' . $thumbnail . '</div>';
360 } else {
361 $mediaContainer = '<div class="form-irre-header-cell form-irre-header-icon" id="' . $objectId . '_iconcontainer">' . $iconImg . '</div>';
362 }
363 $header = $mediaContainer . '
364 <div class="form-irre-header-cell form-irre-header-body">' . $label . '</div>
365 <div class="form-irre-header-cell form-irre-header-control t3js-formengine-irre-control">' . $ctrl . '</div>';
366
367 return $header;
368 }
369
370 /**
371 * Render the control-icons for a record header (create new, sorting, delete, disable/enable).
372 * Most of the parts are copy&paste from TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList and
373 * modified for the JavaScript calls here
374 *
375 * @param array $data Current data
376 * @return string The HTML code with the control-icons
377 */
378 protected function renderForeignRecordHeaderControl(array $data)
379 {
380 $rec = $data['databaseRow'];
381 $inlineConfig = $data['inlineParentConfig'];
382 $foreignTable = $inlineConfig['foreign_table'];
383 $languageService = $this->getLanguageService();
384 $backendUser = $this->getBackendUserAuthentication();
385 // Initialize:
386 $cells = array();
387 $additionalCells = array();
388 $isNewItem = substr($rec['uid'], 0, 3) == 'NEW';
389 $isParentExisting = MathUtility::canBeInterpretedAsInteger($data['inlineParentUid']);
390 $tcaTableCtrl = $GLOBALS['TCA'][$foreignTable]['ctrl'];
391 $tcaTableCols = $GLOBALS['TCA'][$foreignTable]['columns'];
392 $isPagesTable = $foreignTable === 'pages';
393 $isSysFileReferenceTable = $foreignTable === 'sys_file_reference';
394 $enableManualSorting = $tcaTableCtrl['sortby'] || $inlineConfig['MM'] || !$data['isOnSymmetricSide']
395 && $inlineConfig['foreign_sortby'] || $data['isOnSymmetricSide'] && $inlineConfig['symmetric_sortby'];
396 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
397 $nameObjectFt = $nameObject . '-' . $foreignTable;
398 $nameObjectFtId = $nameObjectFt . '-' . $rec['uid'];
399 $calcPerms = $backendUser->calcPerms(BackendUtility::readPageAccess($rec['pid'], $backendUser->getPagePermsClause(1)));
400 // If the listed table is 'pages' we have to request the permission settings for each page:
401 $localCalcPerms = false;
402 if ($isPagesTable) {
403 $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid']));
404 }
405 // This expresses the edit permissions for this particular element:
406 $permsEdit = $isPagesTable && $localCalcPerms & Permission::PAGE_EDIT || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT;
407 // Controls: Defines which controls should be shown
408 $enabledControls = $inlineConfig['appearance']['enabledControls'];
409 // Hook: Can disable/enable single controls for specific child records:
410 foreach ($this->hookObjects as $hookObj) {
411 /** @var InlineElementHookInterface $hookObj */
412 $hookObj->renderForeignRecordHeaderControl_preProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $enabledControls);
413 }
414 if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
415 $cells['localize.isLocalizable'] = '<span title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.isLocalizable')) . '">'
416 . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render()
417 . '</span>';
418 }
419 // "Info": (All records)
420 if ($enabledControls['info'] && !$isNewItem) {
421 if ($rec['table_local'] === 'sys_file') {
422 $uid = (int)substr($rec['uid_local'], 9);
423 $table = '_FILE';
424 } else {
425 $uid = $rec['uid'];
426 $table = $foreignTable;
427 }
428 $cells['info'] = '
429 <a class="btn btn-default" href="#" onclick="' . htmlspecialchars(('top.launchView(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($uid) . '); return false;')) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:showInfo')) . '">
430 ' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '
431 </a>';
432 }
433 // If the table is NOT a read-only table, then show these links:
434 if (!$tcaTableCtrl['readOnly'] && !$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
435 // "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):
436 if ($enabledControls['new'] && ($enableManualSorting || $tcaTableCtrl['useColumnsForDefaultValues'])) {
437 if (!$isPagesTable && $calcPerms & Permission::CONTENT_EDIT || $isPagesTable && $calcPerms & Permission::PAGE_NEW) {
438 $onClick = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ',' . GeneralUtility::quoteJSvalue($rec['uid']) . ')';
439 $style = '';
440 if ($inlineConfig['inline']['inlineNewButtonStyle']) {
441 $style = ' style="' . $inlineConfig['inline']['inlineNewButtonStyle'] . '"';
442 }
443 $cells['new'] = '
444 <a class="btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'] . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:new' . ($isPagesTable ? 'Page' : 'Record')))) . '" ' . $style . '>
445 ' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page' : 'document') . '-new', Icon::SIZE_SMALL)->render() . '
446 </a>';
447 }
448 }
449 // "Up/Down" links
450 if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) {
451 // Up
452 $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'1\')';
453 $style = $inlineConfig['inline']['first'] == $rec['uid'] ? 'style="visibility: hidden;"' : '';
454 $cells['sort.up'] = '
455 <a class="btn btn-default sortingUp" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveUp')) . '">
456 ' . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '
457 </a>';
458 // Down
459 $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'-1\')';
460 $style = $inlineConfig['inline']['last'] == $rec['uid'] ? 'style="visibility: hidden;"' : '';
461 $cells['sort.down'] = '
462 <a class="btn btn-default sortingDown" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveDown')) . '">
463 ' . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '
464 </a>';
465 }
466 // "Edit" link:
467 if (($rec['table_local'] === 'sys_file') && !$isNewItem) {
468 $sys_language_uid = 0;
469 if (!empty($rec['sys_language_uid'])) {
470 $sys_language_uid = $rec['sys_language_uid'][0];
471 }
472 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
473 ->getQueryBuilderForTable('sys_file_metadata');
474 $recordInDatabase = $queryBuilder
475 ->select('uid')
476 ->from('sys_file_metadata')
477 ->where(
478 $queryBuilder->expr()->eq('file', (int)substr($rec['uid_local'], 9)),
479 $queryBuilder->expr()->eq('sys_language_uid', (int)$sys_language_uid)
480 )
481 ->setMaxResults(1)
482 ->execute()
483 ->fetch();
484 if ($backendUser->check('tables_modify', 'sys_file_metadata')) {
485 $url = BackendUtility::getModuleUrl('record_edit', array(
486 'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit'
487 ));
488 $editOnClick = 'if (top.content.list_frame) {' .
489 'top.content.list_frame.location.href=' .
490 GeneralUtility::quoteJSvalue($url . '&returnUrl=') .
491 '+top.rawurlencode(top.content.list_frame.document.location.pathname+top.content.list_frame.document.location.search)' .
492 ';' .
493 '}';
494 $title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.editMetadata');
495 $cells['editmetadata'] = '
496 <a class="btn btn-default" href="#" class="btn" onclick="' . htmlspecialchars($editOnClick) . '" title="' . htmlspecialchars($title) . '">
497 ' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '
498 </a>';
499 }
500 }
501 // "Delete" link:
502 if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & Permission::PAGE_DELETE
503 || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT
504 || $isSysFileReferenceTable && $calcPerms & Permission::PAGE_EDIT)
505 ) {
506 $title = htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:delete'));
507 $icon = $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render();
508 $cells['delete'] = '<a href="#" class="btn btn-default t3js-editform-delete-inline-record" data-objectid="' . htmlspecialchars($nameObjectFtId) . '" title="' . $title . '">' . $icon . '</a>';
509 }
510
511 // "Hide/Unhide" links:
512 $hiddenField = $tcaTableCtrl['enablecolumns']['disabled'];
513 if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))) {
514 $onClick = 'return inline.enableDisableRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ',' .
515 GeneralUtility::quoteJSvalue($hiddenField) . ')';
516 $className = 't3js-' . $nameObjectFtId . '_disabled';
517 if ($rec[$hiddenField]) {
518 $title = htmlspecialchars($languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : ''))));
519 $cells['hide.unhide'] = '
520 <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="'
521 . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">' .
522 $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '
523 </a>';
524 } else {
525 $title = htmlspecialchars($languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : ''))));
526 $cells['hide.hide'] = '
527 <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="'
528 . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">' .
529 $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '
530 </a>';
531 }
532 }
533 // Drag&Drop Sorting: Sortable handler for script.aculo.us
534 if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $inlineConfig['appearance']['useSortable']) {
535 $additionalCells['dragdrop'] = '
536 <span class="btn btn-default sortableHandle" data-id="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.move')) . '">
537 ' . $this->iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '
538 </span>';
539 }
540 } elseif ($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] && $isParentExisting) {
541 if ($enabledControls['localize'] && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
542 $onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
543 $cells['localize'] = '
544 <a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize')) . '">
545 ' . $this->iconFactory->getIcon('actions-document-localize', Icon::SIZE_SMALL)->render() . '
546 </a>';
547 }
548 }
549 // If the record is edit-locked by another user, we will show a little warning sign:
550 if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) {
551 $cells['locked'] = '
552 <a class="btn btn-default" href="#" onclick="alert(' . GeneralUtility::quoteJSvalue($lockInfo['msg']) . ');return false;">
553 ' . '<span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $this->iconFactory->getIcon('status-warning-in-use', Icon::SIZE_SMALL)->render() . '</span>' . '
554 </a>';
555 }
556 // Hook: Post-processing of single controls for specific child records:
557 foreach ($this->hookObjects as $hookObj) {
558 $hookObj->renderForeignRecordHeaderControl_postProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $cells);
559 }
560
561 $out = '';
562 if (!empty($cells)) {
563 $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
564 }
565 if (!empty($additionalCells)) {
566 $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $additionalCells) . '</div>';
567 }
568 return $out;
569 }
570
571 /**
572 * Normalize a relation "uid" published by transferData, like "1|Company%201"
573 *
574 * @param string $string A transferData reference string, containing the uid
575 * @return string The normalized uid
576 */
577 protected function normalizeUid($string)
578 {
579 $parts = explode('|', $string);
580 return $parts[0];
581 }
582
583 /**
584 * Initialized the hook objects for this class.
585 * Each hook object has to implement the interface
586 * \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
587 *
588 * @throws \UnexpectedValueException
589 * @return void
590 */
591 protected function initHookObjects()
592 {
593 $this->hookObjects = array();
594 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'])) {
595 $tceformsInlineHook = &$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'];
596 if (is_array($tceformsInlineHook)) {
597 foreach ($tceformsInlineHook as $classData) {
598 $processObject = GeneralUtility::getUserObj($classData);
599 if (!$processObject instanceof InlineElementHookInterface) {
600 throw new \UnexpectedValueException($classData . ' must implement interface ' . InlineElementHookInterface::class, 1202072000);
601 }
602 $this->hookObjects[] = $processObject;
603 }
604 }
605 }
606 }
607
608 /**
609 * @return BackendUserAuthentication
610 */
611 protected function getBackendUserAuthentication()
612 {
613 return $GLOBALS['BE_USER'];
614 }
615
616 /**
617 * @return DatabaseConnection
618 */
619 protected function getDatabaseConnection()
620 {
621 return $GLOBALS['TYPO3_DB'];
622 }
623
624 /**
625 * @return LanguageService
626 */
627 protected function getLanguageService()
628 {
629 return $GLOBALS['LANG'];
630 }
631 }