1<?php
2namespace TYPO3\CMS\Workspaces\Service;
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
17use Psr\Log\LoggerAwareInterface;
18use Psr\Log\LoggerAwareTrait;
19use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
20use TYPO3\CMS\Backend\Utility\BackendUtility;
21use TYPO3\CMS\Core\Cache\CacheManager;
22use TYPO3\CMS\Core\Imaging\Icon;
23use TYPO3\CMS\Core\Imaging\IconFactory;
24use TYPO3\CMS\Core\Utility\GeneralUtility;
25use TYPO3\CMS\Core\Versioning\VersionState;
26use TYPO3\CMS\Extbase\Object\ObjectManager;
27use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
28use TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord;
29use TYPO3\CMS\Workspaces\Preview\PreviewUriBuilder;
30
31/**
32 * Grid data service
33 */
34class GridDataService implements LoggerAwareInterface
35{
36    use LoggerAwareTrait;
37
38    const SIGNAL_GenerateDataArray_BeforeCaching = 'generateDataArray.beforeCaching';
39    const SIGNAL_GenerateDataArray_PostProcesss = 'generateDataArray.postProcess';
40    const SIGNAL_GetDataArray_PostProcesss = 'getDataArray.postProcess';
41    const SIGNAL_SortDataArray_PostProcesss = 'sortDataArray.postProcess';
42
43    const GridColumn_Collection = 'Workspaces_Collection';
44    const GridColumn_CollectionLevel = 'Workspaces_CollectionLevel';
45    const GridColumn_CollectionParent = 'Workspaces_CollectionParent';
46    const GridColumn_CollectionCurrent = 'Workspaces_CollectionCurrent';
47    const GridColumn_CollectionChildren = 'Workspaces_CollectionChildren';
48
49    /**
50     * Id of the current active workspace.
51     *
52     * @var int
53     */
54    protected $currentWorkspace;
55
56    /**
57     * Version record information (filtered, sorted and limited)
58     *
59     * @var array
60     */
61    protected $dataArray = [];
62
63    /**
64     * Name of the field used for sorting.
65     *
66     * @var string
67     */
68    protected $sort = '';
69
70    /**
71     * Direction used for sorting (ASC, DESC).
72     *
73     * @var string
74     */
75    protected $sortDir = '';
76
77    /**
78     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
79     */
80    protected $workspacesCache;
81
82    /**
83     * @var IntegrityService
84     */
85    protected $integrityService;
86
87    /**
88     * Generates grid list array from given versions.
89     *
90     * @param array $versions All records uids etc. First key is table name, second key incremental integer. Records are associative arrays with uid and t3ver_oid fields. The pid of the online record is found as "livepid" the pid of the offline record is found in "wspid
91     * @param \stdClass $parameter Parameters as submitted by JavaScript component
92     * @param int $currentWorkspace The current workspace
93     * @return array Version record information (filtered, sorted and limited)
94     * @throws \InvalidArgumentException
95     */
96    public function generateGridListFromVersions($versions, $parameter, $currentWorkspace)
97    {
98        // Read the given parameters from grid. If the parameter is not set use default values.
99        $filterTxt = $parameter->filterTxt ?? '';
100        $start = isset($parameter->start) ? (int)$parameter->start : 0;
101        $limit = isset($parameter->limit) ? (int)$parameter->limit : 30;
102        $this->sort = $parameter->sort ?? 't3ver_oid';
103        $this->sortDir = $parameter->dir ?? 'ASC';
104        if (is_int($currentWorkspace)) {
105            $this->currentWorkspace = $currentWorkspace;
106        } else {
107            throw new \InvalidArgumentException('No such workspace defined', 1476048304);
108        }
109        $data = [];
110        $data['data'] = [];
111        $this->generateDataArray($versions, $filterTxt);
112        $data['total'] = count($this->dataArray);
113        $data['data'] = $this->getDataArray($start, $limit);
114        return $data;
115    }
116
117    /**
118     * Generates grid list array from given versions.
119     *
120     * @param array $versions All available version records
121     * @param string $filterTxt Text to be used to filter record result
122     */
123    protected function generateDataArray(array $versions, $filterTxt)
124    {
125        $workspaceAccess = $GLOBALS['BE_USER']->checkWorkspace($GLOBALS['BE_USER']->workspace);
126        $swapStage = $workspaceAccess['publish_access'] & 1 ? StagesService::STAGE_PUBLISH_ID : 0;
127        $swapAccess = $GLOBALS['BE_USER']->workspacePublishAccess($GLOBALS['BE_USER']->workspace) && $GLOBALS['BE_USER']->workspaceSwapAccess();
128        $this->initializeWorkspacesCachingFramework();
129        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
130        // check for dataArray in cache
131        if ($this->getDataArrayFromCache($versions, $filterTxt) === false) {
132            $stagesObj = GeneralUtility::makeInstance(StagesService::class);
133            $defaultGridColumns = [
134                self::GridColumn_Collection => 0,
135                self::GridColumn_CollectionLevel => 0,
136                self::GridColumn_CollectionParent => '',
137                self::GridColumn_CollectionCurrent => '',
138                self::GridColumn_CollectionChildren => 0,
139            ];
140            foreach ($versions as $table => $records) {
141                $hiddenField = $this->getTcaEnableColumnsFieldName($table, 'disabled');
142                $isRecordTypeAllowedToModify = $GLOBALS['BE_USER']->check('tables_modify', $table);
143
144                foreach ($records as $record) {
145                    $origRecord = BackendUtility::getRecord($table, $record['t3ver_oid']);
146                    $versionRecord = BackendUtility::getRecord($table, $record['uid']);
147                    $combinedRecord = CombinedRecord::createFromArrays($table, $origRecord, $versionRecord);
148                    $this->getIntegrityService()->checkElement($combinedRecord);
149
150                    if ($hiddenField !== null) {
151                        $recordState = $this->workspaceState($versionRecord['t3ver_state'], $origRecord[$hiddenField], $versionRecord[$hiddenField]);
152                    } else {
153                        $recordState = $this->workspaceState($versionRecord['t3ver_state']);
154                    }
155
156                    $isDeletedPage = $table === 'pages' && $recordState === 'deleted';
157                    $pageId = $table === 'pages' ? $record['uid'] : $record['pid'];
158                    $viewUrl = GeneralUtility::makeInstance(PreviewUriBuilder::class)->buildUriForElement($table, $record['uid'], $origRecord, $versionRecord);
159                    $versionArray = [];
160                    $versionArray['table'] = $table;
161                    $versionArray['id'] = $table . ':' . $record['uid'];
162                    $versionArray['uid'] = $record['uid'];
163                    $versionArray['workspace'] = $versionRecord['t3ver_id'];
164                    $versionArray = array_merge($versionArray, $defaultGridColumns);
165                    $versionArray['label_Workspace'] = htmlspecialchars(BackendUtility::getRecordTitle($table, $versionRecord));
166                    $versionArray['label_Live'] = htmlspecialchars(BackendUtility::getRecordTitle($table, $origRecord));
167                    $versionArray['label_Stage'] = htmlspecialchars($stagesObj->getStageTitle($versionRecord['t3ver_stage']));
168                    $tempStage = $stagesObj->getNextStage($versionRecord['t3ver_stage']);
169                    $versionArray['label_nextStage'] = htmlspecialchars($stagesObj->getStageTitle($tempStage['uid']));
170                    $versionArray['value_nextStage'] = (int)$tempStage['uid'];
171                    $tempStage = $stagesObj->getPrevStage($versionRecord['t3ver_stage']);
172                    $versionArray['label_prevStage'] = htmlspecialchars($stagesObj->getStageTitle($tempStage['uid']));
173                    $versionArray['value_prevStage'] = (int)$tempStage['uid'];
174                    $versionArray['path_Live'] = htmlspecialchars(BackendUtility::getRecordPath($record['livepid'], '', 999));
175                    // no htmlspecialchars necessary as this is only used in JS via text function
176                    $versionArray['path_Workspace'] = BackendUtility::getRecordPath($record['wspid'], '', 999);
177                    $versionArray['workspace_Title'] = htmlspecialchars(WorkspaceService::getWorkspaceTitle($versionRecord['t3ver_wsid']));
178                    $versionArray['workspace_Tstamp'] = $versionRecord['tstamp'];
179                    $versionArray['workspace_Formated_Tstamp'] = BackendUtility::datetime($versionRecord['tstamp']);
180                    $versionArray['t3ver_wsid'] = $versionRecord['t3ver_wsid'];
181                    $versionArray['t3ver_oid'] = $record['t3ver_oid'];
182                    $versionArray['livepid'] = $record['livepid'];
183                    $versionArray['stage'] = $versionRecord['t3ver_stage'];
184                    $versionArray['icon_Live'] = $iconFactory->getIconForRecord($table, $origRecord, Icon::SIZE_SMALL)->render();
185                    $versionArray['icon_Workspace'] = $iconFactory->getIconForRecord($table, $versionRecord, Icon::SIZE_SMALL)->render();
186                    $languageValue = $this->getLanguageValue($table, $versionRecord);
187                    $versionArray['languageValue'] = $languageValue;
188                    $versionArray['language'] = [
189                        'icon' => $iconFactory->getIcon($this->getSystemLanguageValue($languageValue, $pageId, 'flagIcon'), Icon::SIZE_SMALL)->render()
190                    ];
191                    $versionArray['allowedAction_nextStage'] = $isRecordTypeAllowedToModify && $stagesObj->isNextStageAllowedForUser($versionRecord['t3ver_stage']);
192                    $versionArray['allowedAction_prevStage'] = $isRecordTypeAllowedToModify && $stagesObj->isPrevStageAllowedForUser($versionRecord['t3ver_stage']);
193                    if ($swapAccess && $swapStage != 0 && $versionRecord['t3ver_stage'] == $swapStage) {
194                        $versionArray['allowedAction_swap'] = $isRecordTypeAllowedToModify && $stagesObj->isNextStageAllowedForUser($swapStage);
195                    } elseif ($swapAccess && $swapStage == 0) {
196                        $versionArray['allowedAction_swap'] = $isRecordTypeAllowedToModify;
197                    } else {
198                        $versionArray['allowedAction_swap'] = false;
199                    }
200                    $versionArray['allowedAction_delete'] = $isRecordTypeAllowedToModify;
201                    // preview and editing of a deleted page won't work ;)
202                    $versionArray['allowedAction_view'] = !$isDeletedPage && $viewUrl;
203                    $versionArray['allowedAction_edit'] = $isRecordTypeAllowedToModify && !$isDeletedPage;
204                    $versionArray['allowedAction_editVersionedPage'] = $isRecordTypeAllowedToModify && !$isDeletedPage;
205                    $versionArray['state_Workspace'] = $recordState;
206
207                    $versionArray = array_merge(
208                        $versionArray,
209                        $this->getAdditionalColumnService()->getData($combinedRecord)
210                    );
211
212                    if ($filterTxt == '' || $this->isFilterTextInVisibleColumns($filterTxt, $versionArray)) {
213                        $versionIdentifier = $versionArray['id'];
214                        $this->dataArray[$versionIdentifier] = $versionArray;
215                    }
216                }
217            }
218            // Suggested slot method:
219            // methodName(\TYPO3\CMS\Workspaces\Service\GridDataService $gridData, array $dataArray, array $versions)
220            list($this->dataArray, $versions) = $this->emitSignal(self::SIGNAL_GenerateDataArray_BeforeCaching, $this->dataArray, $versions);
221            // Enrich elements after everything has been processed:
222            foreach ($this->dataArray as &$element) {
223                $identifier = $element['table'] . ':' . $element['t3ver_oid'];
224                $element['integrity'] = [
225                    'status' => $this->getIntegrityService()->getStatusRepresentation($identifier),
226                    'messages' => htmlspecialchars($this->getIntegrityService()->getIssueMessages($identifier, true))
227                ];
228            }
229            $this->setDataArrayIntoCache($versions, $filterTxt);
230        }
231        // Suggested slot method:
232        // methodName(\TYPO3\CMS\Workspaces\Service\GridDataService $gridData, array $dataArray, array $versions)
233        list($this->dataArray, $versions) = $this->emitSignal(self::SIGNAL_GenerateDataArray_PostProcesss, $this->dataArray, $versions);
234        $this->sortDataArray();
235        $this->resolveDataArrayDependencies();
236    }
237
238    /**
239     * Resolves dependencies of nested structures
240     * and sort data elements considering these dependencies.
241     */
242    protected function resolveDataArrayDependencies()
243    {
244        $collectionService = $this->getDependencyCollectionService();
245        $dependencyResolver = $collectionService->getDependencyResolver();
246
247        foreach ($this->dataArray as $dataElement) {
248            $dependencyResolver->addElement($dataElement['table'], $dataElement['uid']);
249        }
250
251        $this->dataArray = $collectionService->process($this->dataArray);
252    }
253
254    /**
255     * Gets the data array by considering the page to be shown in the grid view.
256     *
257     * @param int $start
258     * @param int $limit
259     * @return array
260     */
261    protected function getDataArray($start, $limit)
262    {
263        $dataArrayPart = [];
264        $dataArrayCount = count($this->dataArray);
265        $end = ($start + $limit < $dataArrayCount ? $start + $limit : $dataArrayCount);
266
267        // Ensure that there are numerical indexes
268        $this->dataArray = array_values($this->dataArray);
269        for ($i = $start; $i < $end; $i++) {
270            $dataArrayPart[] = $this->dataArray[$i];
271        }
272
273        // Ensure that collections are not cut for the pagination
274        if (!empty($this->dataArray[$i][self::GridColumn_Collection])) {
275            $collectionIdentifier = $this->dataArray[$i][self::GridColumn_Collection];
276            for ($i = $i + 1; $i < $dataArrayCount && $collectionIdentifier === $this->dataArray[$i][self::GridColumn_Collection]; $i++) {
277                $dataArrayPart[] = $this->dataArray[$i];
278            }
279        }
280
281        // Suggested slot method:
282        // methodName(\TYPO3\CMS\Workspaces\Service\GridDataService $gridData, array $dataArray, $start, $limit, array $dataArrayPart)
283        list($this->dataArray, $start, $limit, $dataArrayPart) = $this->emitSignal(self::SIGNAL_GetDataArray_PostProcesss, $this->dataArray, $start, $limit, $dataArrayPart);
284        return $dataArrayPart;
285    }
286
287    /**
288     * Initializes the workspace cache
289     */
290    protected function initializeWorkspacesCachingFramework()
291    {
292        $this->workspacesCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('workspaces_cache');
293    }
294
295    /**
296     * Puts the generated dataArray into the workspace cache.
297     *
298     * @param array $versions All records uids etc. First key is table name, second key incremental integer. Records are associative arrays with uid and t3ver_oid fields. The pid of the online record is found as "livepid" the pid of the offline record is found in "wspid
299     * @param string $filterTxt The given filter text from the grid.
300     */
301    protected function setDataArrayIntoCache(array $versions, $filterTxt)
302    {
303        $hash = $this->calculateHash($versions, $filterTxt);
304        $this->workspacesCache->set($hash, $this->dataArray, [(string)$this->currentWorkspace, 'user_' . $GLOBALS['BE_USER']->user['uid']]);
305    }
306
307    /**
308     * Checks if a cache entry is given for given versions and filter text and tries to load the data array from cache.
309     *
310     * @param array $versions All records uids etc. First key is table name, second key incremental integer. Records are associative arrays with uid and t3ver_oid fields. The pid of the online record is found as "livepid" the pid of the offline record is found in "wspid
311     * @param string $filterTxt The given filter text from the grid.
312     * @return bool TRUE if cache entry was successfully fetched from cache and content put to $this->dataArray
313     */
314    protected function getDataArrayFromCache(array $versions, $filterTxt)
315    {
316        $cacheEntry = false;
317        $hash = $this->calculateHash($versions, $filterTxt);
318        $content = $this->workspacesCache->get($hash);
319        if ($content !== false) {
320            $this->dataArray = $content;
321            $cacheEntry = true;
322        }
323        return $cacheEntry;
324    }
325
326    /**
327     * Calculates the hash value of the used workspace, the user id, the versions array, the filter text, the sorting attribute, the workspace selected in grid and the sorting direction.
328     *
329     * @param array $versions All records uids etc. First key is table name, second key incremental integer. Records are associative arrays with uid and t3ver_oid fields. The pid of the online record is found as "livepid" the pid of the offline record is found in "wspid
330     * @param string $filterTxt The given filter text from the grid.
331     * @return string
332     */
333    protected function calculateHash(array $versions, $filterTxt)
334    {
335        $hashArray = [
336            $GLOBALS['BE_USER']->workspace,
337            $GLOBALS['BE_USER']->user['uid'],
338            $versions,
339            $filterTxt,
340            $this->sort,
341            $this->sortDir,
342            $this->currentWorkspace
343        ];
344        $hash = md5(serialize($hashArray));
345        return $hash;
346    }
347
348    /**
349     * Performs sorting on the data array accordant to the
350     * selected column in the grid view to be used for sorting.
351     */
352    protected function sortDataArray()
353    {
354        if (is_array($this->dataArray)) {
355            switch ($this->sort) {
356                case 'uid':
357                case 'change':
358                case 'workspace_Tstamp':
359                case 't3ver_oid':
360                case 'liveid':
361                case 'livepid':
362                case 'languageValue':
363                    uasort($this->dataArray, [$this, 'intSort']);
364                    break;
365                case 'label_Workspace':
366                case 'label_Live':
367                case 'label_Stage':
368                case 'workspace_Title':
369                case 'path_Live':
370                    // case 'path_Workspace': This is the first sorting attribute
371                    uasort($this->dataArray, [$this, 'stringSort']);
372                    break;
373                default:
374                    // Do nothing
375            }
376        } else {
377            $this->logger->critical('Try to sort "' . $this->sort . '" in "\\TYPO3\\CMS\\Workspaces\\Service\\GridDataService::sortDataArray" but $this->dataArray is empty! This might be the bug #26422 which could not be reproduced yet.');
378        }
379        // Suggested slot method:
380        // methodName(\TYPO3\CMS\Workspaces\Service\GridDataService $gridData, array $dataArray, $sortColumn, $sortDirection)
381        list($this->dataArray, $this->sort, $this->sortDir) = $this->emitSignal(self::SIGNAL_SortDataArray_PostProcesss, $this->dataArray, $this->sort, $this->sortDir);
382    }
383
384    /**
385     * Implements individual sorting for columns based on integer comparison.
386     *
387     * @param array $a First value
388     * @param array $b Second value
389     * @return int
390     */
391    protected function intSort(array $a, array $b)
392    {
393        if (!$this->isSortable($a, $b)) {
394            return 0;
395        }
396        // First sort by using the page-path in current workspace
397        $path_cmp = strcasecmp($a['path_Workspace'], $b['path_Workspace']);
398        if ($path_cmp < 0) {
399            return $path_cmp;
400        }
401        if ($path_cmp == 0) {
402            if ($a[$this->sort] == $b[$this->sort]) {
403                return 0;
404            }
405            if ($this->sortDir === 'ASC') {
406                return $a[$this->sort] < $b[$this->sort] ? -1 : 1;
407            }
408            if ($this->sortDir === 'DESC') {
409                return $a[$this->sort] > $b[$this->sort] ? -1 : 1;
410            }
411        } elseif ($path_cmp > 0) {
412            return $path_cmp;
413        }
414        return 0;
415    }
416
417    /**
418     * Implements individual sorting for columns based on string comparison.
419     *
420     * @param string $a First value
421     * @param string $b Second value
422     * @return int
423     */
424    protected function stringSort($a, $b)
425    {
426        if (!$this->isSortable($a, $b)) {
427            return 0;
428        }
429        $path_cmp = strcasecmp($a['path_Workspace'], $b['path_Workspace']);
430        if ($path_cmp < 0) {
431            return $path_cmp;
432        }
433        if ($path_cmp == 0) {
434            if ($a[$this->sort] == $b[$this->sort]) {
435                return 0;
436            }
437            if ($this->sortDir === 'ASC') {
438                return strcasecmp($a[$this->sort], $b[$this->sort]);
439            }
440            if ($this->sortDir === 'DESC') {
441                return strcasecmp($a[$this->sort], $b[$this->sort]) * -1;
442            }
443        } elseif ($path_cmp > 0) {
444            return $path_cmp;
445        }
446        return 0;
447    }
448
449    /**
450     * Determines whether dataArray elements are sortable.
451     * Only elements on the first level (0) or below the same
452     * parent element are directly sortable.
453     *
454     * @param array $a
455     * @param array $b
456     * @return bool
457     */
458    protected function isSortable(array $a, array $b)
459    {
460        return
461            $a[self::GridColumn_CollectionLevel] === 0 && $b[self::GridColumn_CollectionLevel] === 0
462            || $a[self::GridColumn_CollectionParent] === $b[self::GridColumn_CollectionParent]
463        ;
464    }
465
466    /**
467     * Determines whether the text used to filter the results is part of
468     * a column that is visible in the grid view.
469     *
470     * @param string $filterText
471     * @param array $versionArray
472     * @return bool
473     */
474    protected function isFilterTextInVisibleColumns($filterText, array $versionArray)
475    {
476        if (is_array($GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['columns'])) {
477            $visibleColumns = $GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['columns'];
478        } else {
479            $visibleColumns = [
480                'workspace_Formated_Tstamp' => ['hidden' => 0],
481                'change' => ['hidden' => 0],
482                'path_Workspace' => ['hidden' => 0],
483                'path_Live' => ['hidden' => 0],
484                'label_Live' => ['hidden' => 0],
485                'label_Stage' => ['hidden' => 0],
486                'label_Workspace' => ['hidden' => 0],
487            ];
488        }
489        foreach ($visibleColumns as $column => $value) {
490            if (isset($value['hidden']) && isset($column) && isset($versionArray[$column])) {
491                if ($value['hidden'] == 0) {
492                    switch ($column) {
493                        case 'workspace_Tstamp':
494                            if (stripos($versionArray['workspace_Formated_Tstamp'], $filterText) !== false) {
495                                return true;
496                            }
497                            break;
498                        case 'change':
499                            if (stripos(strval($versionArray[$column]), str_replace('%', '', $filterText)) !== false) {
500                                return true;
501                            }
502                            break;
503                        default:
504                            if (stripos(strval($versionArray[$column]), $filterText) !== false) {
505                                return true;
506                            }
507                    }
508                }
509            }
510        }
511        return false;
512    }
513
514    /**
515     * Gets the state of a given state value.
516     *
517     * @param int $stateId stateId of offline record
518     * @param bool $hiddenOnline hidden status of online record
519     * @param bool $hiddenOffline hidden status of offline record
520     * @return string
521     */
522    protected function workspaceState($stateId, $hiddenOnline = false, $hiddenOffline = false)
523    {
524        $hiddenState = null;
525        if ($hiddenOnline == 0 && $hiddenOffline == 1) {
526            $hiddenState = 'hidden';
527        } elseif ($hiddenOnline == 1 && $hiddenOffline == 0) {
528            $hiddenState = 'unhidden';
529        }
530        switch ($stateId) {
531            case VersionState::NEW_PLACEHOLDER_VERSION:
532                $state = 'new';
533                break;
534            case VersionState::DELETE_PLACEHOLDER:
535                $state = 'deleted';
536                break;
537            case VersionState::MOVE_POINTER:
538                $state = 'moved';
539                break;
540            default:
541                $state = ($hiddenState ?: 'modified');
542        }
543        return $state;
544    }
545
546    /**
547     * Gets the field name of the enable-columns as defined in $TCA.
548     *
549     * @param string $table Name of the table
550     * @param string $type Type to be fetches (e.g. 'disabled', 'starttime', 'endtime', 'fe_group)
551     * @return string|null The accordant field name or NULL if not defined
552     */
553    protected function getTcaEnableColumnsFieldName($table, $type)
554    {
555        $fieldName = null;
556
557        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'][$type])) {
558            $fieldName = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'][$type];
559        }
560
561        return $fieldName;
562    }
563
564    /**
565     * Gets the used language value (sys_language.uid) of
566     * a given database record.
567     *
568     * @param string $table Name of the table
569     * @param array $record Database record
570     * @return int
571     */
572    protected function getLanguageValue($table, array $record)
573    {
574        $languageValue = 0;
575        if (BackendUtility::isTableLocalizable($table)) {
576            $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
577            if (!empty($record[$languageField])) {
578                $languageValue = $record[$languageField];
579            }
580        }
581        return $languageValue;
582    }
583
584    /**
585     * Gets a named value of the available sys_language elements.
586     *
587     * @param int $id sys_language uid
588     * @param int $pageId page id of a site
589     * @param string $key Name of the value to be fetched (e.g. title)
590     * @return string|null
591     * @see getSystemLanguages
592     */
593    protected function getSystemLanguageValue($id, $pageId, $key)
594    {
595        $value = null;
596        $systemLanguages = $this->getSystemLanguages((int)$pageId);
597        if (!empty($systemLanguages[$id][$key])) {
598            $value = $systemLanguages[$id][$key];
599        }
600        return $value;
601    }
602
603    /**
604     * Gets all available system languages.
605     *
606     * @param int $pageId
607     * @return array
608     */
609    public function getSystemLanguages(int $pageId)
610    {
611        return GeneralUtility::makeInstance(TranslationConfigurationProvider::class)->getSystemLanguages($pageId);
612    }
613
614    /**
615     * Gets an instance of the integrity service.
616     *
617     * @return IntegrityService
618     */
619    protected function getIntegrityService()
620    {
621        if (!isset($this->integrityService)) {
622            $this->integrityService = GeneralUtility::makeInstance(IntegrityService::class);
623        }
624        return $this->integrityService;
625    }
626
627    /**
628     * Emits a signal to be handled by any registered slots.
629     *
630     * @param string $signalName Name of the signal
631     * @param array<int, mixed> $arguments
632     * @return array
633     */
634    protected function emitSignal($signalName, ...$arguments)
635    {
636        // Arguments are always ($this, [method argument], [method argument], ...)
637        $signalArguments = $arguments;
638        array_unshift($signalArguments, $this);
639        $slotReturn = $this->getSignalSlotDispatcher()->dispatch(GridDataService::class, $signalName, $signalArguments);
640        return array_slice($slotReturn, 1);
641    }
642
643    /**
644     * @return Dependency\CollectionService
645     */
646    protected function getDependencyCollectionService()
647    {
648        return GeneralUtility::makeInstance(Dependency\CollectionService::class);
649    }
650
651    /**
652     * @return AdditionalColumnService
653     */
654    protected function getAdditionalColumnService()
655    {
656        return $this->getObjectManager()->get(AdditionalColumnService::class);
657    }
658
659    /**
660     * @return Dispatcher
661     */
662    protected function getSignalSlotDispatcher()
663    {
664        return $this->getObjectManager()->get(Dispatcher::class);
665    }
666
667    /**
668     * @return ObjectManager
669     */
670    protected function getObjectManager()
671    {
672        return GeneralUtility::makeInstance(ObjectManager::class);
673    }
674}
675