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
16namespace TYPO3\CMS\Workspaces\Service;
17
18use TYPO3\CMS\Backend\Utility\BackendUtility;
19use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
20use TYPO3\CMS\Core\Database\Connection;
21use TYPO3\CMS\Core\Database\ConnectionPool;
22use TYPO3\CMS\Core\Localization\LanguageService;
23use TYPO3\CMS\Core\SingletonInterface;
24use TYPO3\CMS\Core\Utility\GeneralUtility;
25use TYPO3\CMS\Core\Utility\MathUtility;
26use TYPO3\CMS\Workspaces\Domain\Record\StageRecord;
27use TYPO3\CMS\Workspaces\Domain\Record\WorkspaceRecord;
28
29/**
30 * Stages service
31 */
32class StagesService implements SingletonInterface
33{
34    const TABLE_STAGE = 'sys_workspace_stage';
35    // if a record is in the "ready to publish" stage STAGE_PUBLISH_ID the nextStage is STAGE_PUBLISH_EXECUTE_ID, this id wont be saved at any time in db
36    const STAGE_PUBLISH_EXECUTE_ID = -20;
37    // ready to publish stage
38    const STAGE_PUBLISH_ID = -10;
39    const STAGE_EDIT_ID = 0;
40
41    /**
42     * Path to the locallang file
43     *
44     * @var string
45     */
46    private $pathToLocallang = 'LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf';
47
48    /**
49     * @var RecordService
50     */
51    protected $recordService;
52
53    /**
54     * Local cache to reduce number of database queries for stages, groups, etc.
55     *
56     * @var array
57     */
58    protected $workspaceStageCache = [];
59
60    /**
61     * @var array
62     */
63    protected $workspaceStageAllowedCache = [];
64
65    /**
66     * @var array
67     */
68    protected $fetchGroupsCache = [];
69
70    /**
71     * @var array
72     */
73    protected $userGroups = [];
74
75    /**
76     * Getter for current workspace id
77     *
78     * @return int Current workspace id
79     */
80    public function getWorkspaceId()
81    {
82        return $this->getBackendUser()->workspace;
83    }
84
85    /**
86     * Find the highest possible "previous" stage for all $byTableName
87     *
88     * @param array $workspaceItems
89     * @param array $byTableName
90     * @return array Current and next highest possible stage
91     */
92    public function getPreviousStageForElementCollection(
93        $workspaceItems,
94        array $byTableName = ['tt_content', 'pages']
95    ) {
96        $currentStage = [];
97        $previousStage = [];
98        $usedStages = [];
99        $found = false;
100        $availableStagesForWS = array_reverse($this->getStagesForWS());
101        $availableStagesForWSUser = $this->getStagesForWSUser();
102        $byTableName = array_flip($byTableName);
103        foreach ($workspaceItems as $tableName => $items) {
104            if (!array_key_exists($tableName, $byTableName)) {
105                continue;
106            }
107            foreach ($items as $item) {
108                $usedStages[$item['t3ver_stage']] = true;
109            }
110        }
111        foreach ($availableStagesForWS as $stage) {
112            if (isset($usedStages[$stage['uid']])) {
113                $currentStage = $stage;
114                $previousStage = $this->getPrevStage($stage['uid']);
115                break;
116            }
117        }
118        foreach ($availableStagesForWSUser as $userWS) {
119            if ($previousStage['uid'] == $userWS['uid']) {
120                $found = true;
121                break;
122            }
123        }
124        if ($found === false || !$this->isStageAllowedForUser($currentStage['uid'])) {
125            $previousStage = [];
126        }
127        return [
128            $currentStage,
129            $previousStage
130        ];
131    }
132
133    /**
134     * Retrieve the next stage based on the lowest stage given in the $workspaceItems record array.
135     *
136     * @param array $workspaceItems
137     * @param array $byTableName
138     * @return array Current and next possible stage.
139     */
140    public function getNextStageForElementCollection(
141        $workspaceItems,
142        array $byTableName = ['tt_content', 'pages']
143    ) {
144        $currentStage = [];
145        $usedStages = [];
146        $nextStage = [];
147        $availableStagesForWS = $this->getStagesForWS();
148        $availableStagesForWSUser = $this->getStagesForWSUser();
149        $byTableName = array_flip($byTableName);
150        $found = false;
151        foreach ($workspaceItems as $tableName => $items) {
152            if (!array_key_exists($tableName, $byTableName)) {
153                continue;
154            }
155            foreach ($items as $item) {
156                $usedStages[$item['t3ver_stage']] = true;
157            }
158        }
159        foreach ($availableStagesForWS as $stage) {
160            if (isset($usedStages[$stage['uid']])) {
161                $currentStage = $stage;
162                $nextStage = $this->getNextStage($stage['uid']);
163                break;
164            }
165        }
166        foreach ($availableStagesForWSUser as $userWS) {
167            if ($nextStage['uid'] == $userWS['uid']) {
168                $found = true;
169                break;
170            }
171        }
172        if ($found === false || !$this->isStageAllowedForUser($currentStage['uid'])) {
173            $nextStage = [];
174        }
175        return [
176            $currentStage,
177            $nextStage
178        ];
179    }
180
181    /**
182     * Building an array with all stage ids and titles related to the given workspace
183     *
184     * @return array id and title of the stages
185     */
186    public function getStagesForWS()
187    {
188        if (isset($this->workspaceStageCache[$this->getWorkspaceId()])) {
189            $stages = $this->workspaceStageCache[$this->getWorkspaceId()];
190        } elseif ($this->getWorkspaceId() === 0) {
191            $stages = [];
192        } else {
193            $stages = $this->prepareStagesArray($this->getWorkspaceRecord()->getStages());
194            $this->workspaceStageCache[$this->getWorkspaceId()] = $stages;
195        }
196        return $stages;
197    }
198
199    /**
200     * Returns an array of stages, the user is allowed to send to
201     *
202     * @return array id and title of stages
203     */
204    public function getStagesForWSUser()
205    {
206        if ($this->getBackendUser()->isAdmin()) {
207            return $this->getStagesForWS();
208        }
209
210        /** @var StageRecord[] $allowedStages */
211        $allowedStages = [];
212        $stageRecords = $this->getWorkspaceRecord()->getStages();
213
214        // Only use stages that are allowed for current backend user
215        foreach ($stageRecords as $stageRecord) {
216            if ($stageRecord->isAllowed()) {
217                $allowedStages[$stageRecord->getUid()] = $stageRecord;
218            }
219        }
220
221        // Add previous and next stages (even if they are not allowed!)
222        foreach ($allowedStages as $allowedStage) {
223            $previousStage = $allowedStage->getPrevious();
224            $nextStage = $allowedStage->getNext();
225            if ($previousStage !== null && !isset($allowedStages[$previousStage->getUid()])) {
226                $allowedStages[$previousStage->getUid()] = $previousStage;
227            }
228            if ($nextStage !== null && !isset($allowedStages[$nextStage->getUid()])) {
229                $allowedStages[$nextStage->getUid()] = $nextStage;
230            }
231        }
232
233        uasort($allowedStages, function (StageRecord $first, StageRecord $second) {
234            return $first->determineOrder($second);
235        });
236        return $this->prepareStagesArray($allowedStages);
237    }
238
239    /**
240     * Prepares simplified stages array
241     *
242     * @param StageRecord[] $stageRecords
243     * @return array
244     */
245    protected function prepareStagesArray(array $stageRecords)
246    {
247        $stagesArray = [];
248        foreach ($stageRecords as $stageRecord) {
249            $stage = [
250                'uid' => $stageRecord->getUid(),
251                'label' => $stageRecord->getTitle(),
252            ];
253            if (!$stageRecord->isExecuteStage()) {
254                $stage['title'] = $this->getLanguageService()->sL($this->pathToLocallang . ':actionSendToStage') . ' "' . $stageRecord->getTitle() . '"';
255            } else {
256                $stage['title'] = $this->getLanguageService()->sL($this->pathToLocallang . ':publish_execute_action_option');
257            }
258            $stagesArray[] = $stage;
259        }
260        return $stagesArray;
261    }
262
263    /**
264     * Gets the title of a stage.
265     *
266     * @param int $ver_stage
267     * @return string
268     */
269    public function getStageTitle($ver_stage)
270    {
271        switch ($ver_stage) {
272            case self::STAGE_PUBLISH_EXECUTE_ID:
273                $stageTitle = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod_user_ws.xlf:stage_publish');
274                break;
275            case self::STAGE_PUBLISH_ID:
276                $stageTitle = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:stage_ready_to_publish');
277                break;
278            case self::STAGE_EDIT_ID:
279                $stageTitle = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod_user_ws.xlf:stage_editing');
280                break;
281            default:
282                $stageTitle = $this->getPropertyOfCurrentWorkspaceStage($ver_stage, 'title');
283                if ($stageTitle == null) {
284                    $stageTitle = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.getStageTitle.stageNotFound');
285                }
286        }
287        return $stageTitle;
288    }
289
290    /**
291     * Gets a particular stage record.
292     *
293     * @param int $stageid
294     * @return array
295     */
296    public function getStageRecord($stageid)
297    {
298        return BackendUtility::getRecord('sys_workspace_stage', $stageid);
299    }
300
301    /**
302     * Gets next stage in process for given stage id
303     *
304     * @param int $stageId Id of the stage to fetch the next one for
305     * @return array The next stage (id + details)
306     * @throws \InvalidArgumentException
307     */
308    public function getNextStage($stageId)
309    {
310        if (!MathUtility::canBeInterpretedAsInteger($stageId)) {
311            throw new \InvalidArgumentException(
312                $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.stageId.integer'),
313                1291109987
314            );
315        }
316        $nextStage = false;
317        $workspaceStageRecs = $this->getStagesForWS();
318        if (is_array($workspaceStageRecs) && !empty($workspaceStageRecs)) {
319            reset($workspaceStageRecs);
320            while (key($workspaceStageRecs) !== null) {
321                $workspaceStageRec = current($workspaceStageRecs);
322                if ($workspaceStageRec['uid'] == $stageId) {
323                    $nextStage = next($workspaceStageRecs);
324                    break;
325                }
326                next($workspaceStageRecs);
327            }
328        }
329        if ($nextStage === false) {
330            $nextStage[] = [
331                'uid' => self::STAGE_EDIT_ID,
332                'title' => $this->getLanguageService()->sL($this->pathToLocallang . ':actionSendToStage') . ' "'
333                    . $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod_user_ws.xlf:stage_editing') . '"'
334            ];
335        }
336        return $nextStage;
337    }
338
339    /**
340     * Recursive function to get all next stages for a record depending on user permissions
341     *
342     * @param array $nextStageArray Next stages
343     * @param int $stageId Current stage id of the record
344     * @return array Next stages
345     */
346    public function getNextStages(array &$nextStageArray, $stageId)
347    {
348        // Current stage is "Ready to publish" - there is no next stage
349        if ($stageId == self::STAGE_PUBLISH_ID) {
350            return $nextStageArray;
351        }
352        $nextStageRecord = $this->getNextStage($stageId);
353        if (empty($nextStageRecord) || !is_array($nextStageRecord)) {
354            // There is no next stage
355            return $nextStageArray;
356        }
357        // Check if the user has the permission to for the current stage
358        // If this next stage record is the first next stage after the current the user
359        // has always the needed permission
360        if ($this->isStageAllowedForUser($stageId)) {
361            $nextStageArray[] = $nextStageRecord;
362            return $this->getNextStages($nextStageArray, $nextStageRecord['uid']);
363        }
364        // He hasn't - return given next stage array
365        return $nextStageArray;
366    }
367
368    /**
369     * Get next stage in process for given stage id
370     *
371     * @param int $stageId Id of the stage to fetch the previous one for
372     * @return bool|array The previous stage or false
373     * @throws \InvalidArgumentException
374     */
375    public function getPrevStage($stageId)
376    {
377        if (!MathUtility::canBeInterpretedAsInteger($stageId)) {
378            throw new \InvalidArgumentException(
379                $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.stageId.integer'),
380                1476048351
381            );
382        }
383        $prevStage = false;
384        $workspaceStageRecs = $this->getStagesForWS();
385        if (is_array($workspaceStageRecs) && !empty($workspaceStageRecs)) {
386            end($workspaceStageRecs);
387            while (key($workspaceStageRecs) !== null) {
388                $workspaceStageRec = current($workspaceStageRecs);
389                if ($workspaceStageRec['uid'] == $stageId) {
390                    $prevStage = prev($workspaceStageRecs);
391                    break;
392                }
393                prev($workspaceStageRecs);
394            }
395        }
396
397        return $prevStage;
398    }
399
400    /**
401     * Recursive function to get all prev stages for a record depending on user permissions
402     *
403     * @param array	$prevStageArray Prev stages
404     * @param int $stageId Current stage id of the record
405     * @return array prev stages
406     */
407    public function getPrevStages(array &$prevStageArray, $stageId)
408    {
409        // Current stage is "Editing" - there is no prev stage
410        if ($stageId != self::STAGE_EDIT_ID) {
411            $prevStageRecord = $this->getPrevStage($stageId);
412            if (!empty($prevStageRecord) && is_array($prevStageRecord)) {
413                // Check if the user has the permission to switch to that stage
414                // If this prev stage record is the first previous stage before the current
415                // the user has always the needed permission
416                if ($this->isStageAllowedForUser($stageId)) {
417                    $prevStageArray[] = $prevStageRecord;
418                    $prevStageArray = $this->getPrevStages($prevStageArray, $prevStageRecord['uid']);
419                }
420            }
421        }
422        return $prevStageArray;
423    }
424
425    /**
426     * Gets all backend user records that are considered to be responsible
427     * for a particular stage or workspace.
428     *
429     * @param StageRecord|int $stageRecord Stage
430     * @param bool $selectDefaultUserField If field notification_defaults should be selected instead of responsible users
431     * @return array be_users with e-mail and name
432     */
433    public function getResponsibleBeUser($stageRecord, $selectDefaultUserField = false)
434    {
435        if (!$stageRecord instanceof StageRecord) {
436            $stageRecord = $this->getWorkspaceRecord()->getStage($stageRecord);
437        }
438
439        $recipientArray = [];
440
441        if (!$selectDefaultUserField) {
442            $backendUserIds = $stageRecord->getAllRecipients();
443        } else {
444            $backendUserIds = $stageRecord->getDefaultRecipients();
445        }
446
447        $userList = implode(',', $backendUserIds);
448        $userRecords = $this->getBackendUsers($userList);
449        foreach ($userRecords as $userUid => $userRecord) {
450            $recipientArray[$userUid] = $userRecord;
451        }
452        return $recipientArray;
453    }
454
455    /**
456     * Gets backend user ids from a mixed list of backend users
457     * and backend users groups. This is used for notifying persons
458     * responsible for a particular stage or workspace.
459     *
460     * @param string $stageRespValue Responsible_person value from stage record
461     * @return string List of backend user ids
462     */
463    public function getResponsibleUser($stageRespValue)
464    {
465        return implode(',', $this->resolveBackendUserIds($stageRespValue));
466    }
467
468    /**
469     * Resolves backend user ids from a mixed list of backend users
470     * and backend user groups (e.g. "be_users_1,be_groups_3,be_users_4,...")
471     *
472     * @param string $backendUserGroupList
473     * @return array
474     */
475    public function resolveBackendUserIds($backendUserGroupList)
476    {
477        $elements = GeneralUtility::trimExplode(',', $backendUserGroupList, true);
478        $backendUserIds = [];
479        $backendGroupIds = [];
480
481        foreach ($elements as $element) {
482            if (strpos($element, 'be_users_') === 0) {
483                // Current value is a uid of a be_user record
484                $backendUserIds[] = str_replace('be_users_', '', $element);
485            } elseif (strpos($element, 'be_groups_') === 0) {
486                $backendGroupIds[] = str_replace('be_groups_', '', $element);
487            } elseif ((int)$element) {
488                $backendUserIds[] = (int)$element;
489            }
490        }
491
492        if (!empty($backendGroupIds)) {
493            $allBeUserArray = BackendUtility::getUserNames();
494            $backendGroupList = implode(',', $backendGroupIds);
495            $this->userGroups = [];
496            $backendGroups = $this->fetchGroups($backendGroupList);
497            foreach ($backendGroups as $backendGroup) {
498                foreach ($allBeUserArray as $backendUserId => $backendUser) {
499                    if (GeneralUtility::inList($backendUser['usergroup_cached_list'], $backendGroup['uid'])) {
500                        $backendUserIds[] = $backendUserId;
501                    }
502                }
503            }
504        }
505
506        return array_unique($backendUserIds);
507    }
508
509    /**
510     * Gets backend user records from a given list of ids.
511     *
512     * @param string $backendUserList
513     * @return array
514     */
515    public function getBackendUsers($backendUserList)
516    {
517        if (empty($backendUserList)) {
518            return [];
519        }
520
521        $backendUserList = implode(',', GeneralUtility::intExplode(',', $backendUserList));
522        $backendUsers = BackendUtility::getUserNames(
523            'username, uid, email, realName, lang, uc',
524            'AND uid IN (' . $backendUserList . ')' . BackendUtility::BEenableFields('be_users')
525        );
526
527        if (empty($backendUsers)) {
528            $backendUsers = [];
529        }
530        return $backendUsers;
531    }
532
533    /**
534     * @param StageRecord $stageRecord
535     * @return array
536     */
537    public function getPreselectedRecipients(StageRecord $stageRecord)
538    {
539        if ($stageRecord->areEditorsPreselected()) {
540            return array_merge(
541                $stageRecord->getPreselectedRecipients(),
542                $this->getRecordService()->getCreateUserIds()
543            );
544        }
545        return $stageRecord->getPreselectedRecipients();
546    }
547
548    /**
549     * @return WorkspaceRecord
550     */
551    protected function getWorkspaceRecord()
552    {
553        return WorkspaceRecord::get($this->getWorkspaceId());
554    }
555
556    /**
557     * @param string $grList
558     * @param string $idList
559     * @return array
560     */
561    private function fetchGroups($grList, $idList = '')
562    {
563        $cacheKey = md5($grList . $idList);
564        if (isset($this->fetchGroupsCache[$cacheKey])) {
565            return $this->fetchGroupsCache[$cacheKey];
566        }
567        if ($idList === '') {
568            // we're at the beginning of the recursion and therefore we need to reset the userGroups member
569            $this->userGroups = [];
570        }
571        $groupList = $this->fetchGroupsRecursive($grList);
572        $this->fetchGroupsCache[$cacheKey] = $groupList;
573        return $groupList;
574    }
575
576    /**
577     * @param array $groups
578     */
579    private function fetchGroupsFromDB(array $groups)
580    {
581        // The userGroups array is filled
582        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_groups');
583
584        $result = $queryBuilder
585            ->select('*')
586            ->from('be_groups')
587            ->where(
588                $queryBuilder->expr()->in(
589                    'uid',
590                    $queryBuilder->createNamedParameter($groups, Connection::PARAM_INT_ARRAY)
591                )
592            )
593            ->execute();
594
595        while ($row = $result->fetch()) {
596            $this->userGroups[$row['uid']] = $row;
597        }
598    }
599
600    /**
601     * Fetches particular groups recursively.
602     *
603     * @param string $grList
604     * @param string $idList
605     * @return array
606     */
607    private function fetchGroupsRecursive($grList, $idList = '')
608    {
609        $requiredGroups = GeneralUtility::intExplode(',', $grList, true);
610        $existingGroups = array_keys($this->userGroups);
611        $missingGroups = array_diff($requiredGroups, $existingGroups);
612        if (!empty($missingGroups)) {
613            $this->fetchGroupsFromDB($missingGroups);
614        }
615        // Traversing records in the correct order
616        foreach ($requiredGroups as $uid) {
617            // traversing list
618            // Get row:
619            $row = $this->userGroups[$uid];
620            if (is_array($row) && !GeneralUtility::inList($idList, (string)$uid)) {
621                // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
622                // If the localconf.php option isset the user of the sub- sub- groups will also be used
623                if ($GLOBALS['TYPO3_CONF_VARS']['BE']['customStageShowRecipientRecursive'] == 1) {
624                    // Include sub groups
625                    if (trim($row['subgroup'])) {
626                        // Make integer list
627                        $theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
628                        // Get the subgroups
629                        $subGroups = $this->fetchGroups($theList, $idList . ',' . $uid);
630                        // Merge the subgroups to the already existing userGroups array
631                        $subUid = key($subGroups);
632                        $this->userGroups[$subUid] = $subGroups[$subUid];
633                    }
634                }
635            }
636        }
637        return $this->userGroups;
638    }
639
640    /**
641     * Gets a property of a workspaces stage.
642     *
643     * @param int $stageId
644     * @param string $property
645     * @return string
646     * @throws \InvalidArgumentException
647     */
648    public function getPropertyOfCurrentWorkspaceStage($stageId, $property)
649    {
650        $result = null;
651        if (!MathUtility::canBeInterpretedAsInteger($stageId)) {
652            throw new \InvalidArgumentException(
653                $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.stageId.integer'),
654                1476048371
655            );
656        }
657        $workspaceStage = BackendUtility::getRecord(self::TABLE_STAGE, $stageId);
658        if (is_array($workspaceStage) && isset($workspaceStage[$property])) {
659            $result = $workspaceStage[$property];
660        }
661        return $result;
662    }
663
664    /**
665     * Gets the position of the given workspace in the hole process
666     * f.e. 3 means step 3 of 20, by which 1 is edit and 20 is ready to publish
667     *
668     * @param int $stageId
669     * @return array position => 3, count => 20
670     */
671    public function getPositionOfCurrentStage($stageId)
672    {
673        $stagesOfWS = $this->getStagesForWS();
674        $countOfStages = count($stagesOfWS);
675        switch ($stageId) {
676            case self::STAGE_PUBLISH_ID:
677                $position = $countOfStages;
678                break;
679            case self::STAGE_EDIT_ID:
680                $position = 1;
681                break;
682            default:
683                $position = 1;
684                foreach ($stagesOfWS as $key => $stageInfoArray) {
685                    $position++;
686                    if ($stageId == $stageInfoArray['uid']) {
687                        break;
688                    }
689                }
690        }
691        return ['position' => $position, 'count' => $countOfStages];
692    }
693
694    /**
695     * Check if the user has access to the previous stage, relative to the given stage
696     *
697     * @param int $stageId
698     * @return bool
699     */
700    public function isPrevStageAllowedForUser($stageId)
701    {
702        $isAllowed = false;
703        try {
704            $prevStage = $this->getPrevStage($stageId);
705            // if there's no prev-stage the stageIds match,
706            // otherwise we've to check if the user is permitted to use the stage
707            if (!empty($prevStage) && $prevStage['uid'] != $stageId) {
708                // if the current stage is allowed for the user, the user is also allowed to send to prev
709                $isAllowed = $this->isStageAllowedForUser($stageId);
710            }
711        } catch (\Exception $e) {
712        }
713        return $isAllowed;
714    }
715
716    /**
717     * Check if the user has access to the next stage, relative to the given stage
718     *
719     * @param int $stageId
720     * @return bool
721     */
722    public function isNextStageAllowedForUser($stageId)
723    {
724        $isAllowed = false;
725        try {
726            $nextStage = $this->getNextStage($stageId);
727            // if there's no next-stage the stageIds match,
728            // otherwise we've to check if the user is permitted to use the stage
729            if (!empty($nextStage) && $nextStage['uid'] != $stageId) {
730                // if the current stage is allowed for the user, the user is also allowed to send to next
731                $isAllowed = $this->isStageAllowedForUser($stageId);
732            }
733        } catch (\Exception $e) {
734        }
735        return $isAllowed;
736    }
737
738    /**
739     * @param int $stageId
740     * @return bool
741     */
742    protected function isStageAllowedForUser($stageId)
743    {
744        $cacheKey = $this->getWorkspaceId() . '_' . $stageId;
745        if (isset($this->workspaceStageAllowedCache[$cacheKey])) {
746            return $this->workspaceStageAllowedCache[$cacheKey];
747        }
748        $isAllowed = $this->getBackendUser()->workspaceCheckStageForCurrent($stageId);
749        $this->workspaceStageAllowedCache[$cacheKey] = $isAllowed;
750        return $isAllowed;
751    }
752
753    /**
754     * Determines whether a stage Id is valid.
755     *
756     * @param int $stageId The stage Id to be checked
757     * @return bool
758     */
759    public function isValid($stageId)
760    {
761        $isValid = false;
762        $stages = $this->getStagesForWS();
763        foreach ($stages as $stage) {
764            if ($stage['uid'] == $stageId) {
765                $isValid = true;
766                break;
767            }
768        }
769        return $isValid;
770    }
771
772    /**
773     * @return RecordService
774     */
775    public function getRecordService()
776    {
777        if (!isset($this->recordService)) {
778            $this->recordService = GeneralUtility::makeInstance(RecordService::class);
779        }
780        return $this->recordService;
781    }
782
783    /**
784     * @return BackendUserAuthentication
785     */
786    protected function getBackendUser()
787    {
788        return $GLOBALS['BE_USER'];
789    }
790
791    /**
792     * @return LanguageService|null
793     */
794    protected function getLanguageService(): ?LanguageService
795    {
796        return $GLOBALS['LANG'] ?? null;
797    }
798}
799