Passed
Push — master ( 8fe7ba...f8e4b4 )
by
unknown
14:09
created

GridDataService::fillDataArrayPart()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 26
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 11
c 1
b 0
f 0
dl 0
loc 26
rs 9.6111
cc 5
nc 4
nop 2
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Workspaces\Service;
17
18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use Psr\Log\LoggerAwareInterface;
20
use Psr\Log\LoggerAwareTrait;
21
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
22
use TYPO3\CMS\Backend\Utility\BackendUtility;
23
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
24
use TYPO3\CMS\Core\Cache\CacheManager;
25
use TYPO3\CMS\Core\Imaging\Icon;
26
use TYPO3\CMS\Core\Imaging\IconFactory;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28
use TYPO3\CMS\Core\Versioning\VersionState;
29
use TYPO3\CMS\Workspaces\Controller\Remote\RemoteServer;
30
use TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord;
31
use TYPO3\CMS\Workspaces\Event\AfterCompiledCacheableDataForWorkspaceEvent;
32
use TYPO3\CMS\Workspaces\Event\AfterDataGeneratedForWorkspaceEvent;
33
use TYPO3\CMS\Workspaces\Event\GetVersionedDataEvent;
34
use TYPO3\CMS\Workspaces\Event\SortVersionedDataEvent;
35
use TYPO3\CMS\Workspaces\Preview\PreviewUriBuilder;
36
use TYPO3\CMS\Workspaces\Service\Dependency\CollectionService;
37
38
/**
39
 * Grid data service
40
 */
41
class GridDataService implements LoggerAwareInterface
42
{
43
    use LoggerAwareTrait;
44
45
    const GridColumn_Collection = 'Workspaces_Collection';
46
    const GridColumn_CollectionLevel = 'Workspaces_CollectionLevel';
47
    const GridColumn_CollectionParent = 'Workspaces_CollectionParent';
48
    const GridColumn_CollectionCurrent = 'Workspaces_CollectionCurrent';
49
    const GridColumn_CollectionChildren = 'Workspaces_CollectionChildren';
50
51
    /**
52
     * Id of the current active workspace.
53
     *
54
     * @var int
55
     */
56
    protected $currentWorkspace;
57
58
    /**
59
     * Version record information (filtered, sorted and limited)
60
     *
61
     * @var array
62
     */
63
    protected $dataArray = [];
64
65
    /**
66
     * Name of the field used for sorting.
67
     *
68
     * @var string
69
     */
70
    protected $sort = '';
71
72
    /**
73
     * Direction used for sorting (ASC, DESC).
74
     *
75
     * @var string
76
     */
77
    protected $sortDir = '';
78
79
    /**
80
     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
81
     */
82
    protected $workspacesCache;
83
84
    /**
85
     * @var IntegrityService
86
     */
87
    protected $integrityService;
88
89
    /**
90
     * @var EventDispatcherInterface
91
     */
92
    protected $eventDispatcher;
93
94
    public function __construct(EventDispatcherInterface $eventDispatcher)
95
    {
96
        $this->eventDispatcher = $eventDispatcher;
97
    }
98
99
    /**
100
     * Generates grid list array from given versions.
101
     *
102
     * @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
103
     * @param \stdClass $parameter Parameters as submitted by JavaScript component
104
     * @param int $currentWorkspace The current workspace
105
     * @return array Version record information (filtered, sorted and limited)
106
     * @throws \InvalidArgumentException
107
     */
108
    public function generateGridListFromVersions($versions, $parameter, $currentWorkspace)
109
    {
110
        // Read the given parameters from grid. If the parameter is not set use default values.
111
        $filterTxt = $parameter->filterTxt ?? '';
112
        $start = isset($parameter->start) ? (int)$parameter->start : 0;
113
        $limit = isset($parameter->limit) ? (int)$parameter->limit : 30;
114
        $this->sort = $parameter->sort ?? 't3ver_oid';
115
        $this->sortDir = $parameter->dir ?? 'ASC';
116
        if (is_int($currentWorkspace)) {
0 ignored issues
show
introduced by
The condition is_int($currentWorkspace) is always true.
Loading history...
117
            $this->currentWorkspace = $currentWorkspace;
118
        } else {
119
            throw new \InvalidArgumentException('No such workspace defined', 1476048304);
120
        }
121
        $data = [];
122
        $data['data'] = [];
123
        $this->generateDataArray($versions, $filterTxt);
124
        // Only count parent records for pagination
125
        $data['total'] = count(array_filter($this->dataArray, static function ($element) {
126
            return (int)($element[self::GridColumn_CollectionLevel] ?? 0) === 0;
127
        }));
128
        $data['data'] = $this->getDataArray($start, $limit);
129
        return $data;
130
    }
131
132
    /**
133
     * Generates grid list array from given versions.
134
     *
135
     * @param array $versions All available version records
136
     * @param string $filterTxt Text to be used to filter record result
137
     */
138
    protected function generateDataArray(array $versions, $filterTxt)
139
    {
140
        $backendUser = $this->getBackendUser();
141
        $workspaceAccess = $backendUser->checkWorkspace($backendUser->workspace);
142
        $swapStage = $workspaceAccess['publish_access'] & 1 ? StagesService::STAGE_PUBLISH_ID : 0;
143
        $swapAccess = $backendUser->workspacePublishAccess($backendUser->workspace);
144
        $this->initializeWorkspacesCachingFramework();
145
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
146
        // check for dataArray in cache
147
        if ($this->getDataArrayFromCache($versions, $filterTxt) === false) {
148
            $stagesObj = GeneralUtility::makeInstance(StagesService::class);
149
            $defaultGridColumns = [
150
                self::GridColumn_Collection => 0,
151
                self::GridColumn_CollectionLevel => 0,
152
                self::GridColumn_CollectionParent => '',
153
                self::GridColumn_CollectionCurrent => '',
154
                self::GridColumn_CollectionChildren => 0,
155
            ];
156
            foreach ($versions as $table => $records) {
157
                $table = (string)$table;
158
                $hiddenField = $this->getTcaEnableColumnsFieldName($table, 'disabled');
159
                $isRecordTypeAllowedToModify = $backendUser->check('tables_modify', $table);
160
161
                foreach ($records as $record) {
162
                    $origRecord = (array)BackendUtility::getRecord($table, $record['t3ver_oid']);
163
                    $versionRecord = (array)BackendUtility::getRecord($table, $record['uid']);
164
                    $combinedRecord = CombinedRecord::createFromArrays($table, $origRecord, $versionRecord);
165
                    $hasDiff = $this->versionIsModified($combinedRecord);
166
                    $this->getIntegrityService()->checkElement($combinedRecord);
167
168
                    if ($hiddenField !== null) {
169
                        $recordState = $this->workspaceState($versionRecord['t3ver_state'], $origRecord[$hiddenField], $versionRecord[$hiddenField], $hasDiff);
170
                    } else {
171
                        $recordState = $this->workspaceState($versionRecord['t3ver_state'], $hasDiff);
172
                    }
173
174
                    $isDeletedPage = $table === 'pages' && $recordState === 'deleted';
175
                    $pageId = $table === 'pages' ? $record['uid'] : $record['pid'];
176
                    $viewUrl = GeneralUtility::makeInstance(PreviewUriBuilder::class)->buildUriForElement($table, $record['uid'], $origRecord, $versionRecord);
177
                    $workspaceRecordLabel = BackendUtility::getRecordTitle($table, $versionRecord);
178
                    $liveRecordLabel = BackendUtility::getRecordTitle($table, $origRecord);
179
                    [$pathWorkspaceCropped, $pathWorkspace] = BackendUtility::getRecordPath((int)$record['wspid'], '', 15, 1000);
180
                    $calculatedT3verOid = $record['t3ver_oid'];
181
                    if ((int)$record['t3ver_state'] === VersionState::NEW_PLACEHOLDER) {
182
                        // If we're dealing with a 'new' record, this one has no t3ver_oid. On publish, there is no
183
                        // live counterpart, but the publish methods later need a live uid to publish to. We thus
184
                        // use the uid as t3ver_oid here to be transparent on javascript side.
185
                        $calculatedT3verOid = $record['uid'];
186
                    }
187
188
                    $versionArray = [];
189
                    $versionArray['table'] = $table;
190
                    $versionArray['id'] = $table . ':' . $record['uid'];
191
                    $versionArray['uid'] = $record['uid'];
192
                    $versionArray = array_merge($versionArray, $defaultGridColumns);
193
                    $versionArray['label_Workspace'] = htmlspecialchars($workspaceRecordLabel);
194
                    $versionArray['label_Workspace_crop'] = htmlspecialchars(GeneralUtility::fixed_lgd_cs($workspaceRecordLabel, $backendUser->uc['titleLen']));
195
                    $versionArray['label_Live'] = htmlspecialchars($liveRecordLabel);
196
                    $versionArray['label_Live_crop'] = htmlspecialchars(GeneralUtility::fixed_lgd_cs($liveRecordLabel, $backendUser->uc['titleLen']));
197
                    $versionArray['label_Stage'] = htmlspecialchars($stagesObj->getStageTitle($versionRecord['t3ver_stage']));
198
                    $tempStage = $stagesObj->getNextStage($versionRecord['t3ver_stage']);
199
                    $versionArray['label_nextStage'] = htmlspecialchars($stagesObj->getStageTitle($tempStage['uid']));
200
                    $versionArray['value_nextStage'] = (int)$tempStage['uid'];
201
                    $tempStage = $stagesObj->getPrevStage($versionRecord['t3ver_stage']);
202
                    $versionArray['label_prevStage'] = htmlspecialchars($stagesObj->getStageTitle($tempStage['uid']));
203
                    $versionArray['value_prevStage'] = (int)$tempStage['uid'];
204
                    $versionArray['path_Live'] = htmlspecialchars(BackendUtility::getRecordPath($record['livepid'], '', 999));
0 ignored issues
show
Bug introduced by
It seems like TYPO3\CMS\Backend\Utilit...rd['livepid'], '', 999) can also be of type array<integer,string>; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

204
                    $versionArray['path_Live'] = htmlspecialchars(/** @scrutinizer ignore-type */ BackendUtility::getRecordPath($record['livepid'], '', 999));
Loading history...
205
                    $versionArray['path_Workspace'] = htmlspecialchars($pathWorkspace);
206
                    $versionArray['path_Workspace_crop'] = htmlspecialchars($pathWorkspaceCropped);
207
                    $versionArray['workspace_Title'] = htmlspecialchars(WorkspaceService::getWorkspaceTitle($versionRecord['t3ver_wsid']));
208
                    $versionArray['workspace_Tstamp'] = $versionRecord['tstamp'];
209
                    $versionArray['workspace_Formated_Tstamp'] = BackendUtility::datetime($versionRecord['tstamp']);
210
                    $versionArray['t3ver_wsid'] = $versionRecord['t3ver_wsid'];
211
                    $versionArray['t3ver_oid'] = $calculatedT3verOid;
212
                    $versionArray['livepid'] = $record['livepid'];
213
                    $versionArray['stage'] = $versionRecord['t3ver_stage'];
214
                    $versionArray['icon_Live'] = $iconFactory->getIconForRecord($table, $origRecord, Icon::SIZE_SMALL)->render();
215
                    $versionArray['icon_Workspace'] = $iconFactory->getIconForRecord($table, $versionRecord, Icon::SIZE_SMALL)->render();
216
                    $languageValue = $this->getLanguageValue($table, $versionRecord);
217
                    $versionArray['languageValue'] = $languageValue;
218
                    $versionArray['language'] = [
219
                        'icon' => $iconFactory->getIcon($this->getSystemLanguageValue($languageValue, $pageId, 'flagIcon'), Icon::SIZE_SMALL)->render()
220
                    ];
221
                    $versionArray['allowedAction_nextStage'] = $isRecordTypeAllowedToModify && $stagesObj->isNextStageAllowedForUser($versionRecord['t3ver_stage']);
222
                    $versionArray['allowedAction_prevStage'] = $isRecordTypeAllowedToModify && $stagesObj->isPrevStageAllowedForUser($versionRecord['t3ver_stage']);
223
                    if ($swapAccess && $swapStage != 0 && $versionRecord['t3ver_stage'] == $swapStage) {
224
                        $versionArray['allowedAction_publish'] = $isRecordTypeAllowedToModify && $stagesObj->isNextStageAllowedForUser($swapStage);
225
                    } elseif ($swapAccess && $swapStage == 0) {
226
                        $versionArray['allowedAction_publish'] = $isRecordTypeAllowedToModify;
227
                    } else {
228
                        $versionArray['allowedAction_publish'] = false;
229
                    }
230
                    $versionArray['allowedAction_delete'] = $isRecordTypeAllowedToModify;
231
                    // preview and editing of a deleted page won't work ;)
232
                    $versionArray['allowedAction_view'] = !$isDeletedPage && $viewUrl;
233
                    $versionArray['allowedAction_edit'] = $isRecordTypeAllowedToModify && !$isDeletedPage;
234
                    $versionArray['allowedAction_editVersionedPage'] = $isRecordTypeAllowedToModify && !$isDeletedPage;
235
                    $versionArray['state_Workspace'] = $recordState;
236
                    $versionArray['hasChanges'] = ($recordState === 'unchanged') ? false: true;
237
                    // Allows to be overridden by PSR-14 event to dynamically modify the expand / collapse state
238
                    $versionArray['expanded'] = false;
239
240
                    if ($filterTxt == '' || $this->isFilterTextInVisibleColumns($filterTxt, $versionArray)) {
241
                        $versionIdentifier = $versionArray['id'];
242
                        $this->dataArray[$versionIdentifier] = $versionArray;
243
                    }
244
                }
245
            }
246
247
            // Trigger a PSR-14 event
248
            $event = new AfterCompiledCacheableDataForWorkspaceEvent($this, $this->dataArray, $versions);
249
            $this->eventDispatcher->dispatch($event);
250
            $this->dataArray = $event->getData();
251
            $versions = $event->getVersions();
252
            // Enrich elements after everything has been processed:
253
            foreach ($this->dataArray as &$element) {
254
                $identifier = $element['table'] . ':' . $element['t3ver_oid'];
255
                $element['integrity'] = [
256
                    'status' => $this->getIntegrityService()->getStatusRepresentation($identifier),
257
                    'messages' => htmlspecialchars((string)$this->getIntegrityService()->getIssueMessages($identifier, true))
258
                ];
259
            }
260
            $this->setDataArrayIntoCache($versions, $filterTxt);
261
        }
262
263
        // Trigger a PSR-14 event
264
        $event = new AfterDataGeneratedForWorkspaceEvent($this, $this->dataArray, $versions);
265
        $this->eventDispatcher->dispatch($event);
266
        $this->dataArray = $event->getData();
267
        $this->sortDataArray();
268
        $this->resolveDataArrayDependencies();
269
    }
270
271
    protected function versionIsModified(CombinedRecord $combinedRecord): bool
272
    {
273
        $remoteServer = GeneralUtility::makeInstance(RemoteServer::class);
274
275
        $params = new \StdClass();
276
        $params->stage = $combinedRecord->getVersionRecord()->getRow()['t3ver_stage'];
277
        $params->t3ver_oid = $combinedRecord->getLiveRecord()->getUid();
278
        $params->table = $combinedRecord->getLiveRecord()->getTable();
279
        $params->uid = $combinedRecord->getVersionRecord()->getUid();
280
281
        $result = $remoteServer->getRowDetails($params);
282
        return !empty($result['data'][0]['diff']);
283
    }
284
285
    /**
286
     * Resolves dependencies of nested structures
287
     * and sort data elements considering these dependencies.
288
     */
289
    protected function resolveDataArrayDependencies()
290
    {
291
        $collectionService = $this->getDependencyCollectionService();
292
        $dependencyResolver = $collectionService->getDependencyResolver();
293
294
        foreach ($this->dataArray as $dataElement) {
295
            $dependencyResolver->addElement($dataElement['table'], $dataElement['uid']);
296
        }
297
298
        $this->dataArray = $collectionService->process($this->dataArray);
299
    }
300
301
    /**
302
     * Gets the data array by considering the page to be shown in the grid view.
303
     *
304
     * @param int $start
305
     * @param int $limit
306
     * @return array
307
     */
308
    protected function getDataArray($start, $limit)
309
    {
310
        $dataArrayCount = count($this->dataArray);
311
        $start = $this->calculateStartWithCollections($start);
312
        $end = ($start + $limit < $dataArrayCount ? $start + $limit : $dataArrayCount);
313
314
        // Ensure that there are numerical indexes
315
        $this->dataArray = array_values($this->dataArray);
316
        // Fill the data array part
317
        $dataArrayPart = $this->fillDataArrayPart($start, $end);
318
319
        // Trigger a PSR-14 event
320
        $event = new GetVersionedDataEvent($this, $this->dataArray, $start, $limit, $dataArrayPart);
321
        $this->eventDispatcher->dispatch($event);
322
        $this->dataArray = $event->getData();
323
        return $event->getDataArrayPart();
324
    }
325
326
    /**
327
     * Initializes the workspace cache
328
     */
329
    protected function initializeWorkspacesCachingFramework()
330
    {
331
        $this->workspacesCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('workspaces_cache');
332
    }
333
334
    /**
335
     * Puts the generated dataArray into the workspace cache.
336
     *
337
     * @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
338
     * @param string $filterTxt The given filter text from the grid.
339
     */
340
    protected function setDataArrayIntoCache(array $versions, $filterTxt)
341
    {
342
        $hash = $this->calculateHash($versions, $filterTxt);
343
        $this->workspacesCache->set(
344
            $hash,
345
            $this->dataArray,
346
            [
347
                (string)$this->currentWorkspace,
348
                'user_' . $this->getBackendUser()->user['uid']
349
            ]
350
        );
351
    }
352
353
    /**
354
     * Checks if a cache entry is given for given versions and filter text and tries to load the data array from cache.
355
     *
356
     * @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
357
     * @param string $filterTxt The given filter text from the grid.
358
     * @return bool TRUE if cache entry was successfully fetched from cache and content put to $this->dataArray
359
     */
360
    protected function getDataArrayFromCache(array $versions, $filterTxt)
361
    {
362
        $cacheEntry = false;
363
        $hash = $this->calculateHash($versions, $filterTxt);
364
        $content = $this->workspacesCache->get($hash);
365
        if ($content !== false) {
366
            $this->dataArray = $content;
367
            $cacheEntry = true;
368
        }
369
        return $cacheEntry;
370
    }
371
372
    /**
373
     * 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.
374
     *
375
     * @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
376
     * @param string $filterTxt The given filter text from the grid.
377
     * @return string
378
     */
379
    protected function calculateHash(array $versions, $filterTxt)
380
    {
381
        $backendUser = $this->getBackendUser();
382
        $hashArray = [
383
            $backendUser->workspace,
384
            $backendUser->user['uid'],
385
            $versions,
386
            $filterTxt,
387
            $this->sort,
388
            $this->sortDir,
389
            $this->currentWorkspace
390
        ];
391
        $hash = md5(serialize($hashArray));
392
        return $hash;
393
    }
394
395
    /**
396
     * Performs sorting on the data array accordant to the
397
     * selected column in the grid view to be used for sorting.
398
     */
399
    protected function sortDataArray()
400
    {
401
        if (is_array($this->dataArray)) {
0 ignored issues
show
introduced by
The condition is_array($this->dataArray) is always true.
Loading history...
402
            switch ($this->sort) {
403
                case 'uid':
404
                case 'change':
405
                case 'workspace_Tstamp':
406
                case 't3ver_oid':
407
                case 'liveid':
408
                case 'livepid':
409
                case 'languageValue':
410
                    uasort($this->dataArray, [$this, 'intSort']);
411
                    break;
412
                case 'label_Workspace':
413
                case 'label_Live':
414
                case 'label_Stage':
415
                case 'workspace_Title':
416
                case 'path_Live':
417
                    // case 'path_Workspace': This is the first sorting attribute
418
                    uasort($this->dataArray, [$this, 'stringSort']);
419
                    break;
420
                default:
421
                    // Do nothing
422
            }
423
        } else {
424
            $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.');
425
        }
426
        // Trigger an event for extensibility
427
        $event = new SortVersionedDataEvent($this, $this->dataArray, $this->sort, $this->sortDir);
428
        $this->eventDispatcher->dispatch($event);
429
        $this->dataArray = $event->getData();
430
        $this->sort = $event->getSortColumn();
431
        $this->sortDir = $event->getSortDirection();
432
    }
433
434
    /**
435
     * Implements individual sorting for columns based on integer comparison.
436
     *
437
     * @param array $a First value
438
     * @param array $b Second value
439
     * @return int
440
     */
441
    protected function intSort(array $a, array $b)
442
    {
443
        if (!$this->isSortable($a, $b)) {
444
            return 0;
445
        }
446
        // First sort by using the page-path in current workspace
447
        $path_cmp = strcasecmp($a['path_Workspace'], $b['path_Workspace']);
448
        if ($path_cmp < 0) {
449
            return $path_cmp;
450
        }
451
        if ($path_cmp == 0) {
452
            if ($a[$this->sort] == $b[$this->sort]) {
453
                return 0;
454
            }
455
            if ($this->sortDir === 'ASC') {
456
                return $a[$this->sort] < $b[$this->sort] ? -1 : 1;
457
            }
458
            if ($this->sortDir === 'DESC') {
459
                return $a[$this->sort] > $b[$this->sort] ? -1 : 1;
460
            }
461
        } elseif ($path_cmp > 0) {
462
            return $path_cmp;
463
        }
464
        return 0;
465
    }
466
467
    /**
468
     * Implements individual sorting for columns based on string comparison.
469
     *
470
     * @param array $a First value
471
     * @param array $b Second value
472
     * @return int
473
     */
474
    protected function stringSort($a, $b)
475
    {
476
        if (!$this->isSortable($a, $b)) {
477
            return 0;
478
        }
479
        $path_cmp = strcasecmp($a['path_Workspace'], $b['path_Workspace']);
480
        if ($path_cmp < 0) {
481
            return $path_cmp;
482
        }
483
        if ($path_cmp == 0) {
484
            if ($a[$this->sort] == $b[$this->sort]) {
485
                return 0;
486
            }
487
            if ($this->sortDir === 'ASC') {
488
                return strcasecmp($a[$this->sort], $b[$this->sort]);
489
            }
490
            if ($this->sortDir === 'DESC') {
491
                return strcasecmp($a[$this->sort], $b[$this->sort]) * -1;
492
            }
493
        } elseif ($path_cmp > 0) {
494
            return $path_cmp;
495
        }
496
        return 0;
497
    }
498
499
    /**
500
     * Determines whether dataArray elements are sortable.
501
     * Only elements on the first level (0) or below the same
502
     * parent element are directly sortable.
503
     *
504
     * @param array $a
505
     * @param array $b
506
     * @return bool
507
     */
508
    protected function isSortable(array $a, array $b)
509
    {
510
        return
511
            $a[self::GridColumn_CollectionLevel] === 0 && $b[self::GridColumn_CollectionLevel] === 0
512
            || $a[self::GridColumn_CollectionParent] === $b[self::GridColumn_CollectionParent]
513
        ;
514
    }
515
516
    /**
517
     * Determines whether the text used to filter the results is part of
518
     * a column that is visible in the grid view.
519
     *
520
     * @param string $filterText
521
     * @param array $versionArray
522
     * @return bool
523
     */
524
    protected function isFilterTextInVisibleColumns($filterText, array $versionArray)
525
    {
526
        $backendUser = $this->getBackendUser();
527
        if (is_array($backendUser->uc['moduleData']['Workspaces'][$backendUser->workspace]['columns'])) {
528
            $visibleColumns = $backendUser->uc['moduleData']['Workspaces'][$backendUser->workspace]['columns'];
529
        } else {
530
            $visibleColumns = [
531
                'workspace_Formated_Tstamp' => ['hidden' => 0],
532
                'change' => ['hidden' => 0],
533
                'path_Workspace' => ['hidden' => 0],
534
                'path_Live' => ['hidden' => 0],
535
                'label_Live' => ['hidden' => 0],
536
                'label_Stage' => ['hidden' => 0],
537
                'label_Workspace' => ['hidden' => 0],
538
            ];
539
        }
540
        foreach ($visibleColumns as $column => $value) {
541
            if (isset($value['hidden']) && isset($column) && isset($versionArray[$column])) {
542
                if ($value['hidden'] == 0) {
543
                    switch ($column) {
544
                        case 'workspace_Tstamp':
545
                            if (stripos($versionArray['workspace_Formated_Tstamp'], $filterText) !== false) {
546
                                return true;
547
                            }
548
                            break;
549
                        case 'change':
550
                            if (stripos((string)$versionArray[$column], str_replace('%', '', $filterText)) !== false) {
551
                                return true;
552
                            }
553
                            break;
554
                        default:
555
                            if (stripos((string)$versionArray[$column], $filterText) !== false) {
556
                                return true;
557
                            }
558
                    }
559
                }
560
            }
561
        }
562
        return false;
563
    }
564
565
    /**
566
     * Gets the state of a given state value.
567
     *
568
     * @param int $stateId        stateId of offline record
569
     * @param bool $hiddenOnline  hidden status of online record
570
     * @param bool $hiddenOffline hidden status of offline record
571
     * @param bool $hasDiff    whether the version has any changes
572
     *
573
     * @return string
574
     */
575
    protected function workspaceState($stateId, $hiddenOnline = false, $hiddenOffline = false, $hasDiff = true)
576
    {
577
        $hiddenState = null;
578
        if ($hiddenOnline == 0 && $hiddenOffline == 1) {
579
            $hiddenState = 'hidden';
580
        } elseif ($hiddenOnline == 1 && $hiddenOffline == 0) {
581
            $hiddenState = 'unhidden';
582
        }
583
        switch ($stateId) {
584
            case VersionState::NEW_PLACEHOLDER:
585
                $state = 'new';
586
                break;
587
            case VersionState::DELETE_PLACEHOLDER:
588
                $state = 'deleted';
589
                break;
590
            case VersionState::MOVE_POINTER:
591
                $state = 'moved';
592
                break;
593
            default:
594
                if (!$hasDiff) {
595
                    $state =  'unchanged';
596
                } else {
597
                    $state = ($hiddenState ?: 'modified');
598
                }
599
        }
600
601
        return $state;
602
    }
603
604
    /**
605
     * Gets the field name of the enable-columns as defined in $TCA.
606
     *
607
     * @param string $table Name of the table
608
     * @param string $type Type to be fetches (e.g. 'disabled', 'starttime', 'endtime', 'fe_group)
609
     * @return string|null The accordant field name or NULL if not defined
610
     */
611
    protected function getTcaEnableColumnsFieldName($table, $type)
612
    {
613
        $fieldName = null;
614
615
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'][$type])) {
616
            $fieldName = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'][$type];
617
        }
618
619
        return $fieldName;
620
    }
621
622
    /**
623
     * Gets the used language value (sys_language.uid) of
624
     * a given database record.
625
     *
626
     * @param string $table Name of the table
627
     * @param array $record Database record
628
     * @return int
629
     */
630
    protected function getLanguageValue($table, array $record)
631
    {
632
        $languageValue = 0;
633
        if (BackendUtility::isTableLocalizable($table)) {
634
            $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
635
            if (!empty($record[$languageField])) {
636
                $languageValue = $record[$languageField];
637
            }
638
        }
639
        return $languageValue;
640
    }
641
642
    /**
643
     * Gets a named value of the available sys_language elements.
644
     *
645
     * @param int $id sys_language uid
646
     * @param int $pageId page id of a site
647
     * @param string $key Name of the value to be fetched (e.g. title)
648
     * @return string|null
649
     * @see getSystemLanguages
650
     */
651
    protected function getSystemLanguageValue($id, $pageId, $key)
652
    {
653
        $value = null;
654
        $systemLanguages = $this->getSystemLanguages((int)$pageId);
655
        if (!empty($systemLanguages[$id][$key])) {
656
            $value = $systemLanguages[$id][$key];
657
        }
658
        return $value;
659
    }
660
661
    /**
662
     * Calculate the "real" start value by also taking collection children into account
663
     *
664
     * @param int $start
665
     * @return int
666
     */
667
    protected function calculateStartWithCollections(int $start): int
668
    {
669
        // The recordsCount is the real record items count, while the
670
        // parentRecordsCount only takes the parent records into account
671
        $recordsCount = $parentRecordsCount = 0;
672
        while ($parentRecordsCount < $start) {
673
            // Loop over the dataArray until we found enough parent records
674
            $item = $this->dataArray[$recordsCount];
675
            if (($item[self::GridColumn_CollectionLevel] ?? 0) === 0) {
676
                // In case the current parent record is the last one ($start is reached),
677
                // ensure its collection children are counted as well.
678
                if (($parentRecordsCount + 1) === $start && (int)($item[self::GridColumn_Collection] ?? 0) !== 0) {
679
                    // By not providing the third parameter, we only count the collection children recursively
680
                    $this->addCollectionChildrenRecursive($item, $recordsCount);
681
                }
682
                // Only increase the parent records count in case $item is a parent record
683
                $parentRecordsCount++;
684
            }
685
            // Always increase the record items count
686
            $recordsCount++;
687
        }
688
689
        return $recordsCount;
690
    }
691
692
    /**
693
     * Fill the data array part until enough parent records are found ($end is reached).
694
     * Also adds the related collection children, but without increasing the corresponding
695
     * parent records count.
696
     *
697
     * @param int $start
698
     * @param int $end
699
     * @return array
700
     */
701
    private function fillDataArrayPart(int $start, int $end): array
702
    {
703
        // Initialize empty data array part
704
        $dataArrayPart = [];
705
        // The recordsCount is the real record items count, while the
706
        // parentRecordsCount only takes the parent records into account.
707
        $itemsCount = $parentRecordsCount = $start;
708
        while ($parentRecordsCount < $end) {
709
            // Loop over the dataArray until we found enough parent records
710
            $item = $this->dataArray[$itemsCount];
711
            // Add the item to the $dataArrayPart
712
            $dataArrayPart[] = $item;
713
            if (($item[self::GridColumn_CollectionLevel] ?? 0) === 0) {
714
                // In case the current parent record is the last one ($end is reached),
715
                // ensure its collection children are added as well.
716
                if (($parentRecordsCount + 1) === $end && (int)($item[self::GridColumn_Collection] ?? 0) !== 0) {
717
                    // Add collection children recursively
718
                    $this->addCollectionChildrenRecursive($item, $itemsCount, $dataArrayPart);
719
                }
720
                // Only increase the parent records count in case $item is a parent record
721
                $parentRecordsCount++;
722
            }
723
            // Always increase the record items count
724
            $itemsCount++;
725
        }
726
        return $dataArrayPart;
727
    }
728
729
    /**
730
     * Add collection children to the data array part recursively
731
     *
732
     * @param array $item
733
     * @param int $recordsCount
734
     * @param array $dataArrayPart
735
     */
736
    protected function addCollectionChildrenRecursive(array $item, int &$recordsCount, array &$dataArrayPart = []): void
737
    {
738
        $collectionParent = (string)$item[self::GridColumn_CollectionCurrent];
739
        foreach ($this->dataArray as $element) {
740
            if ((string)($element[self::GridColumn_CollectionParent] ?? '') === $collectionParent) {
741
                // Increase the "real" record items count
742
                $recordsCount++;
743
                // Fetch the children from the dataArray using the current record items
744
                // count. This is possible since the dataArray is already sorted.
745
                $child = $this->dataArray[$recordsCount];
746
                // In case $dataArrayPart is not given, just count the item
747
                if ($dataArrayPart !== []) {
748
                    // Add the children
749
                    $dataArrayPart[] = $child;
750
                }
751
                // In case the $child is also a collection, add it's children as well (recursively)
752
                if ((int)($child[self::GridColumn_Collection] ?? 0) !== 0) {
753
                    $this->addCollectionChildrenRecursive($child, $recordsCount, $dataArrayPart);
754
                }
755
            }
756
        }
757
    }
758
759
    /**
760
     * Gets all available system languages.
761
     *
762
     * @param int $pageId
763
     * @return array
764
     */
765
    public function getSystemLanguages(int $pageId)
766
    {
767
        return GeneralUtility::makeInstance(TranslationConfigurationProvider::class)->getSystemLanguages($pageId);
768
    }
769
770
    /**
771
     * Gets an instance of the integrity service.
772
     *
773
     * @return IntegrityService
774
     */
775
    protected function getIntegrityService()
776
    {
777
        if (!isset($this->integrityService)) {
778
            $this->integrityService = GeneralUtility::makeInstance(IntegrityService::class);
779
        }
780
        return $this->integrityService;
781
    }
782
783
    /**
784
     * @return Dependency\CollectionService
785
     */
786
    protected function getDependencyCollectionService()
787
    {
788
        return GeneralUtility::makeInstance(CollectionService::class);
789
    }
790
791
    /**
792
     * @return BackendUserAuthentication
793
     */
794
    protected function getBackendUser()
795
    {
796
        return $GLOBALS['BE_USER'];
797
    }
798
}
799