TYPO3 CMS  TYPO3_8-7
StagesService.php
Go to the documentation of this file.
1 <?php
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 
24 
29 {
30  const TABLE_STAGE = 'sys_workspace_stage';
31  // 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
33  // ready to publish stage
34  const STAGE_PUBLISH_ID = -10;
35  const STAGE_EDIT_ID = 0;
37  const MODE_NOTIFY_ALL = 1;
39 
45  private $pathToLocallang = 'LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf';
46 
50  protected $recordService;
51 
57  protected $workspaceStageCache = [];
58 
63 
67  protected $fetchGroupsCache = [];
68 
72  protected $userGroups = [];
73 
79  public function getWorkspaceId()
80  {
81  return $this->getBackendUser()->workspace;
82  }
83 
92  $workspaceItems,
93  array $byTableName = ['tt_content', 'pages', 'pages_language_overlay']
94  ) {
95  $currentStage = [];
96  $previousStage = [];
97  $usedStages = [];
98  $found = false;
99  $availableStagesForWS = array_reverse($this->getStagesForWS());
100  $availableStagesForWSUser = $this->getStagesForWSUser();
101  $byTableName = array_flip($byTableName);
102  foreach ($workspaceItems as $tableName => $items) {
103  if (!array_key_exists($tableName, $byTableName)) {
104  continue;
105  }
106  foreach ($items as $item) {
107  $usedStages[$item['t3ver_stage']] = true;
108  }
109  }
110  foreach ($availableStagesForWS as $stage) {
111  if (isset($usedStages[$stage['uid']])) {
112  $currentStage = $stage;
113  $previousStage = $this->getPrevStage($stage['uid']);
114  break;
115  }
116  }
117  foreach ($availableStagesForWSUser as $userWS) {
118  if ($previousStage['uid'] == $userWS['uid']) {
119  $found = true;
120  break;
121  }
122  }
123  if ($found === false || !$this->isStageAllowedForUser($currentStage['uid'])) {
124  $previousStage = [];
125  }
126  return [
127  $currentStage,
128  $previousStage
129  ];
130  }
131 
140  $workspaceItems,
141  array $byTableName = ['tt_content', 'pages', 'pages_language_overlay']
142  ) {
143  $currentStage = [];
144  $usedStages = [];
145  $nextStage = [];
146  $availableStagesForWS = $this->getStagesForWS();
147  $availableStagesForWSUser = $this->getStagesForWSUser();
148  $byTableName = array_flip($byTableName);
149  $found = false;
150  foreach ($workspaceItems as $tableName => $items) {
151  if (!array_key_exists($tableName, $byTableName)) {
152  continue;
153  }
154  foreach ($items as $item) {
155  $usedStages[$item['t3ver_stage']] = true;
156  }
157  }
158  foreach ($availableStagesForWS as $stage) {
159  if (isset($usedStages[$stage['uid']])) {
160  $currentStage = $stage;
161  $nextStage = $this->getNextStage($stage['uid']);
162  break;
163  }
164  }
165  foreach ($availableStagesForWSUser as $userWS) {
166  if ($nextStage['uid'] == $userWS['uid']) {
167  $found = true;
168  break;
169  }
170  }
171  if ($found === false || !$this->isStageAllowedForUser($currentStage['uid'])) {
172  $nextStage = [];
173  }
174  return [
175  $currentStage,
176  $nextStage
177  ];
178  }
179 
185  public function getStagesForWS()
186  {
187  if (isset($this->workspaceStageCache[$this->getWorkspaceId()])) {
188  $stages = $this->workspaceStageCache[$this->getWorkspaceId()];
189  } elseif ($this->getWorkspaceId() === 0) {
190  $stages = [];
191  } else {
192  $stages = $this->prepareStagesArray($this->getWorkspaceRecord()->getStages());
193  $this->workspaceStageCache[$this->getWorkspaceId()] = $stages;
194  }
195  return $stages;
196  }
197 
203  public function getStagesForWSUser()
204  {
205  if ($GLOBALS['BE_USER']->isAdmin()) {
206  return $this->getStagesForWS();
207  }
208 
210  $allowedStages = [];
211  $stageRecords = $this->getWorkspaceRecord()->getStages();
212 
213  // Only use stages that are allowed for current backend user
214  foreach ($stageRecords as $stageRecord) {
215  if ($stageRecord->isAllowed()) {
216  $allowedStages[$stageRecord->getUid()] = $stageRecord;
217  }
218  }
219 
220  // Add previous and next stages (even if they are not allowed!)
221  foreach ($allowedStages as $allowedStage) {
222  $previousStage = $allowedStage->getPrevious();
223  $nextStage = $allowedStage->getNext();
224  if ($previousStage !== null && !isset($allowedStages[$previousStage->getUid()])) {
225  $allowedStages[$previousStage->getUid()] = $previousStage;
226  }
227  if ($nextStage !== null && !isset($allowedStages[$nextStage->getUid()])) {
228  $allowedStages[$nextStage->getUid()] = $nextStage;
229  }
230  }
231 
232  uasort($allowedStages, function (StageRecord $first, StageRecord $second) {
233  return $first->determineOrder($second);
234  });
235  return $this->prepareStagesArray($allowedStages);
236  }
237 
244  protected function prepareStagesArray(array $stageRecords)
245  {
246  $stagesArray = [];
247  foreach ($stageRecords as $stageRecord) {
248  $stage = [
249  'uid' => $stageRecord->getUid(),
250  'label' => $stageRecord->getTitle(),
251  ];
252  if (!$stageRecord->isExecuteStage()) {
253  $stage['title'] = $GLOBALS['LANG']->sL(($this->pathToLocallang . ':actionSendToStage')) . ' "' . $stageRecord->getTitle() . '"';
254  } else {
255  $stage['title'] = $GLOBALS['LANG']->sL($this->pathToLocallang . ':publish_execute_action_option');
256  }
257  $stagesArray[] = $stage;
258  }
259  return $stagesArray;
260  }
261 
268  public function getStageTitle($ver_stage)
269  {
270  switch ($ver_stage) {
271  case self::STAGE_PUBLISH_EXECUTE_ID:
272  $stageTitle = $GLOBALS['LANG']->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_user_ws.xlf:stage_publish');
273  break;
274  case self::STAGE_PUBLISH_ID:
275  $stageTitle = $GLOBALS['LANG']->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:stage_ready_to_publish');
276  break;
277  case self::STAGE_EDIT_ID:
278  $stageTitle = $GLOBALS['LANG']->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_user_ws.xlf:stage_editing');
279  break;
280  default:
281  $stageTitle = $this->getPropertyOfCurrentWorkspaceStage($ver_stage, 'title');
282  if ($stageTitle == null) {
283  $stageTitle = $GLOBALS['LANG']->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.getStageTitle.stageNotFound');
284  }
285  }
286  return $stageTitle;
287  }
288 
295  public function getStageRecord($stageid)
296  {
297  return BackendUtility::getRecord('sys_workspace_stage', $stageid);
298  }
299 
307  public function getNextStage($stageId)
308  {
310  throw new \InvalidArgumentException(
311  $GLOBALS['LANG']->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.stageId.integer'),
312  1291109987
313  );
314  }
315  $nextStage = false;
316  $workspaceStageRecs = $this->getStagesForWS();
317  if (is_array($workspaceStageRecs) && !empty($workspaceStageRecs)) {
318  reset($workspaceStageRecs);
319  while (!is_null(($workspaceStageRecKey = key($workspaceStageRecs)))) {
320  $workspaceStageRec = current($workspaceStageRecs);
321  if ($workspaceStageRec['uid'] == $stageId) {
322  $nextStage = next($workspaceStageRecs);
323  break;
324  }
325  next($workspaceStageRecs);
326  }
327  }
328  if ($nextStage === false) {
329  $nextStage[] = [
330  'uid' => self::STAGE_EDIT_ID,
331  'title' => $GLOBALS['LANG']->sL(($this->pathToLocallang . ':actionSendToStage')) . ' "'
332  . $GLOBALS['LANG']->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_user_ws.xlf:stage_editing') . '"'
333  ];
334  }
335  return $nextStage;
336  }
337 
345  public function getNextStages(array &$nextStageArray, $stageId)
346  {
347  // Current stage is "Ready to publish" - there is no next stage
348  if ($stageId == self::STAGE_PUBLISH_ID) {
349  return $nextStageArray;
350  }
351  $nextStageRecord = $this->getNextStage($stageId);
352  if (empty($nextStageRecord) || !is_array($nextStageRecord)) {
353  // There is no next stage
354  return $nextStageArray;
355  }
356  // Check if the user has the permission to for the current stage
357  // If this next stage record is the first next stage after the current the user
358  // has always the needed permission
359  if ($this->isStageAllowedForUser($stageId)) {
360  $nextStageArray[] = $nextStageRecord;
361  return $this->getNextStages($nextStageArray, $nextStageRecord['uid']);
362  }
363  // He hasn't - return given next stage array
364  return $nextStageArray;
365  }
366 
374  public function getPrevStage($stageId)
375  {
377  throw new \InvalidArgumentException(
378  $GLOBALS['LANG']->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.stageId.integer'),
379  1476048351
380  );
381  }
382  $prevStage = false;
383  $workspaceStageRecs = $this->getStagesForWS();
384  if (is_array($workspaceStageRecs) && !empty($workspaceStageRecs)) {
385  end($workspaceStageRecs);
386  while (!is_null(($workspaceStageRecKey = key($workspaceStageRecs)))) {
387  $workspaceStageRec = current($workspaceStageRecs);
388  if ($workspaceStageRec['uid'] == $stageId) {
389  $prevStage = prev($workspaceStageRecs);
390  break;
391  }
392  prev($workspaceStageRecs);
393  }
394  }
395 
396  return $prevStage;
397  }
398 
406  public function getPrevStages(array &$prevStageArray, $stageId)
407  {
408  // Current stage is "Editing" - there is no prev stage
409  if ($stageId != self::STAGE_EDIT_ID) {
410  $prevStageRecord = $this->getPrevStage($stageId);
411  if (!empty($prevStageRecord) && is_array($prevStageRecord)) {
412  // Check if the user has the permission to switch to that stage
413  // If this prev stage record is the first previous stage before the current
414  // the user has always the needed permission
415  if ($this->isStageAllowedForUser($stageId)) {
416  $prevStageArray[] = $prevStageRecord;
417  $prevStageArray = $this->getPrevStages($prevStageArray, $prevStageRecord['uid']);
418  }
419  }
420  }
421  return $prevStageArray;
422  }
423 
432  public function getResponsibleBeUser($stageRecord, $selectDefaultUserField = false)
433  {
434  if (!$stageRecord instanceof StageRecord) {
435  $stageRecord = $this->getWorkspaceRecord()->getStage($stageRecord);
436  }
437 
438  $recipientArray = [];
439 
440  if (!$selectDefaultUserField) {
441  $backendUserIds = $stageRecord->getAllRecipients();
442  } else {
443  $backendUserIds = $stageRecord->getDefaultRecipients();
444  }
445 
446  $userList = implode(',', $backendUserIds);
447  $userRecords = $this->getBackendUsers($userList);
448  foreach ($userRecords as $userUid => $userRecord) {
449  $recipientArray[$userUid] = $userRecord;
450  }
451  return $recipientArray;
452  }
453 
462  public function getResponsibleUser($stageRespValue)
463  {
464  return implode(',', $this->resolveBackendUserIds($stageRespValue));
465  }
466 
474  public function resolveBackendUserIds($backendUserGroupList)
475  {
476  $elements = GeneralUtility::trimExplode(',', $backendUserGroupList, true);
477  $backendUserIds = [];
478  $backendGroupIds = [];
479 
480  foreach ($elements as $element) {
481  if (strpos($element, 'be_users_') === 0) {
482  // Current value is a uid of a be_user record
483  $backendUserIds[] = str_replace('be_users_', '', $element);
484  } elseif (strpos($element, 'be_groups_') === 0) {
485  $backendGroupIds[] = str_replace('be_groups_', '', $element);
486  } elseif ((int)$element) {
487  $backendUserIds[] = (int)$element;
488  }
489  }
490 
491  if (!empty($backendGroupIds)) {
492  $allBeUserArray = BackendUtility::getUserNames();
493  $backendGroupList = implode(',', $backendGroupIds);
494  $this->userGroups = [];
495  $backendGroups = $this->fetchGroups($backendGroupList);
496  foreach ($backendGroups as $backendGroup) {
497  foreach ($allBeUserArray as $backendUserId => $backendUser) {
498  if (GeneralUtility::inList($backendUser['usergroup_cached_list'], $backendGroup['uid'])) {
499  $backendUserIds[] = $backendUserId;
500  }
501  }
502  }
503  }
504 
505  return array_unique($backendUserIds);
506  }
507 
514  public function getBackendUsers($backendUserList)
515  {
516  if (empty($backendUserList)) {
517  return [];
518  }
519 
520  $backendUserList = implode(',', GeneralUtility::intExplode(',', $backendUserList));
521  $backendUsers = BackendUtility::getUserNames(
522  'username, uid, email, realName, lang, uc',
523  'AND uid IN (' . $backendUserList . ')' . BackendUtility::BEenableFields('be_users')
524  );
525 
526  if (empty($backendUsers)) {
527  $backendUsers = [];
528  }
529  return $backendUsers;
530  }
531 
536  public function getPreselectedRecipients(StageRecord $stageRecord)
537  {
538  if ($stageRecord->areEditorsPreselected()) {
539  return array_merge(
540  $stageRecord->getPreselectedRecipients(),
541  $this->getRecordService()->getCreateUserIds()
542  );
543  }
544  return $stageRecord->getPreselectedRecipients();
545  }
546 
550  protected function getWorkspaceRecord()
551  {
552  return WorkspaceRecord::get($this->getWorkspaceId());
553  }
554 
560  private function fetchGroups($grList, $idList = '')
561  {
562  $cacheKey = md5($grList . $idList);
563  $groupList = [];
564  if (isset($this->fetchGroupsCache[$cacheKey])) {
565  $groupList = $this->fetchGroupsCache[$cacheKey];
566  } else {
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  }
574  return $groupList;
575  }
576 
580  private function fetchGroupsFromDB(array $groups)
581  {
582  // The userGroups array is filled
583  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_groups');
584 
585  $result = $queryBuilder
586  ->select('*')
587  ->from('be_groups')
588  ->where(
589  $queryBuilder->expr()->in(
590  'uid',
591  $queryBuilder->createNamedParameter($groups, Connection::PARAM_INT_ARRAY)
592  )
593  )
594  ->execute();
595 
596  while ($row = $result->fetch()) {
597  $this->userGroups[$row['uid']] = $row;
598  }
599  }
600 
608  private function fetchGroupsRecursive($grList, $idList = '')
609  {
610  $requiredGroups = GeneralUtility::intExplode(',', $grList, true);
611  $existingGroups = array_keys($this->userGroups);
612  $missingGroups = array_diff($requiredGroups, $existingGroups);
613  if (!empty($missingGroups)) {
614  $this->fetchGroupsFromDB($missingGroups);
615  }
616  // Traversing records in the correct order
617  foreach ($requiredGroups as $uid) {
618  // traversing list
619  // Get row:
620  $row = $this->userGroups[$uid];
621  if (is_array($row) && !GeneralUtility::inList($idList, $uid)) {
622  // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
623  // If the localconf.php option isset the user of the sub- sub- groups will also be used
624  if ($GLOBALS['TYPO3_CONF_VARS']['BE']['customStageShowRecipientRecursive'] == 1) {
625  // Include sub groups
626  if (trim($row['subgroup'])) {
627  // Make integer list
628  $theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
629  // Get the subgroups
630  $subGroups = $this->fetchGroups($theList, $idList . ',' . $uid);
631  // Merge the subgroups to the already existing userGroups array
632  $subUid = key($subGroups);
633  $this->userGroups[$subUid] = $subGroups[$subUid];
634  }
635  }
636  }
637  }
638  return $this->userGroups;
639  }
640 
649  public function getPropertyOfCurrentWorkspaceStage($stageId, $property)
650  {
651  $result = null;
653  throw new \InvalidArgumentException(
654  $GLOBALS['LANG']->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.stageId.integer'),
655  1476048371
656  );
657  }
658  $workspaceStage = BackendUtility::getRecord(self::TABLE_STAGE, $stageId);
659  if (is_array($workspaceStage) && isset($workspaceStage[$property])) {
660  $result = $workspaceStage[$property];
661  }
662  return $result;
663  }
664 
672  public function getPositionOfCurrentStage($stageId)
673  {
674  $stagesOfWS = $this->getStagesForWS();
675  $countOfStages = count($stagesOfWS);
676  switch ($stageId) {
677  case self::STAGE_PUBLISH_ID:
678  $position = $countOfStages;
679  break;
680  case self::STAGE_EDIT_ID:
681  $position = 1;
682  break;
683  default:
684  $position = 1;
685  foreach ($stagesOfWS as $key => $stageInfoArray) {
686  $position++;
687  if ($stageId == $stageInfoArray['uid']) {
688  break;
689  }
690  }
691  }
692  return ['position' => $position, 'count' => $countOfStages];
693  }
694 
701  public function isPrevStageAllowedForUser($stageId)
702  {
703  $isAllowed = false;
704  try {
705  $prevStage = $this->getPrevStage($stageId);
706  // if there's no prev-stage the stageIds match,
707  // otherwise we've to check if the user is permitted to use the stage
708  if (!empty($prevStage) && $prevStage['uid'] != $stageId) {
709  // if the current stage is allowed for the user, the user is also allowed to send to prev
710  $isAllowed = $this->isStageAllowedForUser($stageId);
711  }
712  } catch (\Exception $e) {
713  }
714  return $isAllowed;
715  }
716 
723  public function isNextStageAllowedForUser($stageId)
724  {
725  $isAllowed = false;
726  try {
727  $nextStage = $this->getNextStage($stageId);
728  // if there's no next-stage the stageIds match,
729  // otherwise we've to check if the user is permitted to use the stage
730  if (!empty($nextStage) && $nextStage['uid'] != $stageId) {
731  // if the current stage is allowed for the user, the user is also allowed to send to next
732  $isAllowed = $this->isStageAllowedForUser($stageId);
733  }
734  } catch (\Exception $e) {
735  }
736  return $isAllowed;
737  }
738 
743  protected function isStageAllowedForUser($stageId)
744  {
745  $cacheKey = $this->getWorkspaceId() . '_' . $stageId;
746  $isAllowed = false;
747  if (isset($this->workspaceStageAllowedCache[$cacheKey])) {
748  $isAllowed = $this->workspaceStageAllowedCache[$cacheKey];
749  } else {
750  $isAllowed = $GLOBALS['BE_USER']->workspaceCheckStageForCurrent($stageId);
751  $this->workspaceStageAllowedCache[$cacheKey] = $isAllowed;
752  }
753  return $isAllowed;
754  }
755 
762  public function isValid($stageId)
763  {
764  $isValid = false;
765  $stages = $this->getStagesForWS();
766  foreach ($stages as $stage) {
767  if ($stage['uid'] == $stageId) {
768  $isValid = true;
769  break;
770  }
771  }
772  return $isValid;
773  }
774 
778  public function getRecordService()
779  {
780  if (!isset($this->recordService)) {
781  $this->recordService = GeneralUtility::makeInstance(RecordService::class);
782  }
783  return $this->recordService;
784  }
785 
789  protected function getBackendUser()
790  {
791  return $GLOBALS['BE_USER'];
792  }
793 }
static intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
static BEenableFields($table, $inv=false)
getPreselectedRecipients(StageRecord $stageRecord)
getResponsibleBeUser($stageRecord, $selectDefaultUserField=false)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static makeInstance($className,... $constructorArguments)
getPropertyOfCurrentWorkspaceStage($stageId, $property)
getPrevStages(array &$prevStageArray, $stageId)
static getUserNames($fields='username, usergroup, usergroup_cached_list, uid', $where='')
getPreviousStageForElementCollection( $workspaceItems, array $byTableName=['tt_content', 'pages', 'pages_language_overlay'])
getNextStages(array &$nextStageArray, $stageId)
static getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
getNextStageForElementCollection( $workspaceItems, array $byTableName=['tt_content', 'pages', 'pages_language_overlay'])