TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
version/Classes/Hook/DataHandlerHook.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Version\Hook;
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 
28 
34 {
42  protected $notificationEmailInfo = [];
43 
49  protected $remappedIds = [];
50 
51  /****************************
52  ***** Cmdmap Hooks ******
53  ****************************/
60  public function processCmdmap_beforeStart(DataHandler $dataHandler)
61  {
62  // Reset notification array
63  $this->notificationEmailInfo = [];
64  // Resolve dependencies of version/workspaces actions:
65  $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get();
66  }
67 
79  public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
80  {
81  // custom command "version"
82  if ($command == 'version') {
83  $commandIsProcessed = true;
84  $action = (string)$value['action'];
85  $comment = !empty($value['comment']) ? $value['comment'] : '';
86  $notificationAlternativeRecipients = (isset($value['notificationAlternativeRecipients'])) && is_array($value['notificationAlternativeRecipients']) ? $value['notificationAlternativeRecipients'] : [];
87  switch ($action) {
88  case 'new':
89  $dataHandler->versionizeRecord($table, $id, $value['label']);
90  break;
91  case 'swap':
92  $this->version_swap($table, $id, $value['swapWith'], $value['swapIntoWS'],
93  $dataHandler,
94  $comment,
95  true,
96  $notificationAlternativeRecipients
97  );
98  break;
99  case 'clearWSID':
100  $this->version_clearWSID($table, $id, false, $dataHandler);
101  break;
102  case 'flush':
103  $this->version_clearWSID($table, $id, true, $dataHandler);
104  break;
105  case 'setStage':
106  $elementIds = GeneralUtility::trimExplode(',', $id, true);
107  foreach ($elementIds as $elementId) {
108  $this->version_setStage($table, $elementId, $value['stageId'],
109  $comment,
110  true,
111  $dataHandler,
112  $notificationAlternativeRecipients
113  );
114  }
115  break;
116  default:
117  // Do nothing
118  }
119  }
120  }
121 
129  public function processCmdmap_afterFinish(DataHandler $dataHandler)
130  {
131  // Empty accumulation array:
132  foreach ($this->notificationEmailInfo as $notifItem) {
133  $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $dataHandler, $notifItem['alternativeRecipients']);
134  }
135  // Reset notification array
136  $this->notificationEmailInfo = [];
137  // Reset remapped IDs
138  $this->remappedIds = [];
139  }
140 
151  public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
152  {
153  // only process the hook if it wasn't processed
154  // by someone else before
155  if ($recordWasDeleted) {
156  return;
157  }
158  $recordWasDeleted = true;
159  // For Live version, try if there is a workspace version because if so, rather "delete" that instead
160  // Look, if record is an offline version, then delete directly:
161  if ($record['pid'] != -1) {
162  if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) {
163  $record = $wsVersion;
164  $id = $record['uid'];
165  }
166  }
167  $recordVersionState = VersionState::cast($record['t3ver_state']);
168  // Look, if record is an offline version, then delete directly:
169  if ($record['pid'] == -1) {
170  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
171  // In Live workspace, delete any. In other workspaces there must be match.
172  if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) {
173  $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
174  // Processing can be skipped if a delete placeholder shall be swapped/published
175  // during the current request. Thus it will be deleted later on...
176  $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']);
177  if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid'])
178  && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'])
179  && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
180  && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
181  && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
182  ) {
183  return null;
184  }
185 
186  if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) {
187  // Change normal versioned record to delete placeholder
188  // Happens when an edited record is deleted
189  GeneralUtility::makeInstance(ConnectionPool::class)
190  ->getConnectionForTable($table)
191  ->update(
192  $table,
193  [
194  't3ver_label' => 'DELETED!',
195  't3ver_state' => 2,
196  ],
197  ['uid' => $id]
198  );
199 
200  // Delete localization overlays:
201  $dataHandler->deleteL10nOverlayRecords($table, $id);
202  } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
203  // Delete those in WS 0 + if their live records state was not "Placeholder".
204  $dataHandler->deleteEl($table, $id);
205  // Delete move-placeholder if current version record is a move-to-pointer
206  if ($recordVersionState->equals(VersionState::MOVE_POINTER)) {
207  $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']);
208  if (!empty($movePlaceholder)) {
209  $dataHandler->deleteEl($table, $movePlaceholder['uid']);
210  }
211  }
212  } else {
213  // If live record was placeholder (new/deleted), rather clear
214  // it from workspace (because it clears both version and placeholder).
215  $this->version_clearWSID($table, $id, false, $dataHandler);
216  }
217  } else {
218  $dataHandler->newlog('Tried to delete record from another workspace', 1);
219  }
220  } else {
221  $dataHandler->newlog('Versioning not enabled for record with PID = -1!', 2);
222  }
223  } elseif ($res = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($record['pid'], $table)) {
224  // Look, if record is "online" or in a versionized branch, then delete directly.
225  if ($res > 0) {
226  $dataHandler->deleteEl($table, $id);
227  } else {
228  $dataHandler->newlog('Stage of root point did not allow for deletion', 1);
229  }
230  } elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) {
231  // Placeholders for moving operations are deletable directly.
232  // Get record which its a placeholder for and reset the t3ver_state of that:
233  if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) {
234  // Clear the state flag of the workspace version of the record
235  // Setting placeholder state value for version (so it can know it is currently a new version...)
236 
237  GeneralUtility::makeInstance(ConnectionPool::class)
238  ->getConnectionForTable($table)
239  ->update(
240  $table,
241  [
242  't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE)
243  ],
244  ['uid' => (int)$wsRec['uid']]
245  );
246  }
247  $dataHandler->deleteEl($table, $id);
248  } else {
249  // Otherwise, try to delete by versioning:
250  $copyMappingArray = $dataHandler->copyMappingArray;
251  $dataHandler->versionizeRecord($table, $id, 'DELETED!', true);
252  // Determine newly created versions:
253  // (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
254  $versionizedElements = ArrayUtility::arrayDiffAssocRecursive($dataHandler->copyMappingArray, $copyMappingArray);
255  // Delete localization overlays:
256  foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
257  foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
258  $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
259  }
260  }
261  }
262  }
263 
278  public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
279  {
280  // Only do something in Draft workspace
281  if ($dataHandler->BE_USER->workspace === 0) {
282  return;
283  }
284  if ($destPid < 0) {
285  // Fetch move placeholder, since it might point to a new page in the current workspace
286  $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid');
287  if ($movePlaceHolder !== false) {
288  $resolvedPid = $movePlaceHolder['pid'];
289  }
290  }
291  $recordWasMoved = true;
292  $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']);
293  // Get workspace version of the source record, if any:
294  $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
295  // Handle move-placeholders if the current record is not one already
296  if (
298  && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER)
299  ) {
300  // Create version of record first, if it does not exist
301  if (empty($WSversion['uid'])) {
302  $dataHandler->versionizeRecord($table, $uid, 'MovePointer');
303  $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
304  $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
305  // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
306  } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$WSversion['uid']) {
307  $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
308  }
309  }
310  // Check workspace permissions:
311  $workspaceAccessBlocked = [];
312  // Element was in "New/Deleted/Moved" so it can be moved...
313  $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder();
314  $destRes = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($resolvedPid, $table);
315  $canMoveRecord = ($recIsNewVersion || BackendUtility::isTableWorkspaceEnabled($table));
316  // Workspace source check:
317  if (!$recIsNewVersion) {
318  $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $WSversion['uid'] ? $WSversion['uid'] : $uid);
319  if ($errorCode) {
320  $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
321  } elseif (!$canMoveRecord && $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($moveRec['pid'], $table) <= 0) {
322  $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
323  }
324  }
325  // Workspace destination check:
326  // All records can be inserted if $destRes is greater than zero.
327  // Only new versions can be inserted if $destRes is FALSE.
328  // NO RECORDS can be inserted if $destRes is negative which indicates a stage
329  // not allowed for use. If "versioningWS" is version 2, moving can take place of versions.
330  // since TYPO3 CMS 7, version2 is the default and the only option
331  if (!($destRes > 0 || $canMoveRecord && !$destRes)) {
332  $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
333  } elseif ($destRes == 1 && $WSversion['uid']) {
334  $workspaceAccessBlocked['dest2'] = 'Could not insert other versions in destination PID ';
335  }
336  if (empty($workspaceAccessBlocked)) {
337  // If the move operation is done on a versioned record, which is
338  // NOT new/deleted placeholder and versioningWS is in version 2, then...
339  // since TYPO3 CMS 7, version2 is the default and the only option
340  if ($WSversion['uid'] && !$recIsNewVersion && BackendUtility::isTableWorkspaceEnabled($table)) {
341  $this->moveRecord_wsPlaceholders($table, $uid, $destPid, $WSversion['uid'], $dataHandler);
342  } else {
343  // moving not needed, just behave like in live workspace
344  $recordWasMoved = false;
345  }
346  } else {
347  $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), 1);
348  }
349  }
350 
360  protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
361  {
362  $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
363  if (empty($versionedRecord)) {
364  return;
365  }
366  foreach ($versionedRecord as $field => $value) {
367  if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
368  continue;
369  }
371  $dataHandler, $resolvedPageId,
372  $table, $uid, $field, $value,
373  $GLOBALS['TCA'][$table]['columns'][$field]['config']
374  );
375  }
376  }
377 
390  protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration)
391  {
392  $inlineFieldType = $dataHandler->getInlineFieldType($configuration);
393  $inlineProcessing = (
394  ($inlineFieldType === 'list' || $inlineFieldType === 'field')
395  && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
396  && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
397  );
398 
399  if ($inlineProcessing) {
400  if ($table === 'pages') {
401  // If the inline elements are related to a page record,
402  // make sure they reside at that page and not at its parent
403  $resolvedPageId = $uid;
404  }
405 
406  $dbAnalysis = $this->createRelationHandlerInstance();
407  $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
408 
409  // Moving records to a positive destination will insert each
410  // record at the beginning, thus the order is reversed here:
411  foreach ($dbAnalysis->itemArray as $item) {
412  $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
413  if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) {
414  continue;
415  }
416  $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
417  }
418  }
419  }
420 
421  /****************************
422  ***** Notifications ******
423  ****************************/
436  protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
437  {
438  $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
439  // So, if $id is not set, then $table is taken to be the complete element name!
440  $elementName = $id ? $table . ':' . $id : $table;
441  if (!is_array($workspaceRec)) {
442  return;
443  }
444 
445  // Get the new stage title from workspaces library, if workspaces extension is installed
446  if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) {
447  $stageService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\StagesService::class);
448  $newStage = $stageService->getStageTitle((int)$stageId);
449  } else {
450  // @todo CONSTANTS SHOULD BE USED - tx_service_workspace_workspaces
451  // @todo use localized labels
452  // Compile label:
453  switch ((int)$stageId) {
454  case 1:
455  $newStage = 'Ready for review';
456  break;
457  case 10:
458  $newStage = 'Ready for publishing';
459  break;
460  case -1:
461  $newStage = 'Element was rejected!';
462  break;
463  case 0:
464  $newStage = 'Rejected element was noticed and edited';
465  break;
466  default:
467  $newStage = 'Unknown state change!?';
468  }
469  }
470  if (empty($notificationAlternativeRecipients)) {
471  // Compile list of recipients:
472  $emails = [];
473  switch ((int)$stat['stagechg_notification']) {
474  case 1:
475  switch ((int)$stageId) {
476  case 1:
477  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
478  break;
479  case 10:
480  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
481  break;
482  case -1:
483  // List of elements to reject:
484  $allElements = explode(',', $elementName);
485  // Traverse them, and find the history of each
486  foreach ($allElements as $elRef) {
487  list($eTable, $eUid) = explode(':', $elRef);
488 
489  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
490  ->getQueryBuilderForTable('sys_log');
491 
492  $queryBuilder->getRestrictions()->removeAll();
493 
494  $result = $queryBuilder
495  ->select('log_data', 'tstamp', 'userid')
496  ->from('sys_log')
497  ->where(
498  $queryBuilder->expr()->eq(
499  'action',
500  $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT)
501  ),
502  $queryBuilder->expr()->eq(
503  'details_nr',
504  $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT)
505  ),
506  $queryBuilder->expr()->eq(
507  'tablename',
508  $queryBuilder->createNamedParameter($eTable, \PDO::PARAM_STR)
509  ),
510  $queryBuilder->expr()->eq(
511  'recuid',
512  $queryBuilder->createNamedParameter($eUid, \PDO::PARAM_INT)
513  )
514  )
515  ->orderBy('uid', 'DESC')
516  ->execute();
517 
518  // Find all implicated since the last stage-raise from editing to review:
519  while ($dat = $result->fetch()) {
520  $data = unserialize($dat['log_data']);
521  $emails = $this->getEmailsForStageChangeNotification($dat['userid'], true) + $emails;
522  if ($data['stage'] == 1) {
523  break;
524  }
525  }
526  }
527  break;
528  case 0:
529  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
530  break;
531  default:
532  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
533  }
534  break;
535  case 10:
536  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
537  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']) + $emails;
538  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails;
539  break;
540  default:
541  // Do nothing
542  }
543  } else {
544  $emails = $notificationAlternativeRecipients;
545  }
546  // prepare and then send the emails
547  if (!empty($emails)) {
548  // Path to record is found:
549  list($elementTable, $elementUid) = explode(':', $elementName);
550  $elementUid = (int)$elementUid;
551  $elementRecord = BackendUtility::getRecord($elementTable, $elementUid);
552  $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord);
553  if ($elementTable == 'pages') {
554  $pageUid = $elementUid;
555  } else {
556  BackendUtility::fixVersioningPid($elementTable, $elementRecord);
557  $pageUid = ($elementUid = $elementRecord['pid']);
558  }
559 
560  // new way, options are
561  // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject
562  // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject
563  $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid);
564  $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.'];
565  $markers = [
566  '###RECORD_TITLE###' => $recordTitle,
567  '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20),
568  '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
569  '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir,
570  '###WORKSPACE_TITLE###' => $workspaceRec['title'],
571  '###WORKSPACE_UID###' => $workspaceRec['uid'],
572  '###ELEMENT_NAME###' => $elementName,
573  '###NEXT_STAGE###' => $newStage,
574  '###COMMENT###' => $comment,
575  // See: #30212 - keep both markers for compatibility
576  '###USER_REALNAME###' => $dataHandler->BE_USER->user['realName'],
577  '###USER_FULLNAME###' => $dataHandler->BE_USER->user['realName'],
578  '###USER_USERNAME###' => $dataHandler->BE_USER->user['username']
579  ];
580  // add marker for preview links if workspace extension is loaded
581  if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) {
582  $this->workspaceService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class);
583  // only generate the link if the marker is in the template - prevents database from getting to much entries
584  if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) {
585  $tempEmailMessage = $this->getLanguageService()->sL($emailConfig['message']);
586  } else {
587  $tempEmailMessage = $emailConfig['message'];
588  }
589  if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== false) {
590  $markers['###PREVIEW_LINK###'] = $this->workspaceService->generateWorkspacePreviewLink($elementUid);
591  }
592  unset($tempEmailMessage);
593  $markers['###SPLITTED_PREVIEW_LINK###'] = $this->workspaceService->generateWorkspaceSplittedPreviewLink($elementUid, true);
594  }
595  // Hook for preprocessing of the content for formmails:
596  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'])) {
597  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] as $_classRef) {
598  $_procObj =& GeneralUtility::getUserObj($_classRef);
599  $markers = $_procObj->postModifyMarkers($markers, $this);
600  }
601  }
602  // send an email to each individual user, to ensure the
603  // multilanguage version of the email
604  $emailRecipients = [];
605  // an array of language objects that are needed
606  // for emails with different languages
607  $languageObjects = [
608  $this->getLanguageService()->lang => $this->getLanguageService()
609  ];
610  // loop through each recipient and send the email
611  foreach ($emails as $recipientData) {
612  // don't send an email twice
613  if (isset($emailRecipients[$recipientData['email']])) {
614  continue;
615  }
616  $emailSubject = $emailConfig['subject'];
617  $emailMessage = $emailConfig['message'];
618  $emailRecipients[$recipientData['email']] = $recipientData['email'];
619  // check if the email needs to be localized
620  // in the users' language
621  if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
622  $recipientLanguage = $recipientData['lang'] ? $recipientData['lang'] : 'default';
623  if (!isset($languageObjects[$recipientLanguage])) {
624  // a LANG object in this language hasn't been
625  // instantiated yet, so this is done here
627  $languageObject = GeneralUtility::makeInstance(\TYPO3\CMS\Lang\LanguageService::class);
628  $languageObject->init($recipientLanguage);
629  $languageObjects[$recipientLanguage] = $languageObject;
630  } else {
631  $languageObject = $languageObjects[$recipientLanguage];
632  }
633  if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) {
634  $emailSubject = $languageObject->sL($emailSubject);
635  }
636  if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
637  $emailMessage = $languageObject->sL($emailMessage);
638  }
639  }
640  $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
641  $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true);
642  $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true);
643  // Send an email to the recipient
645  $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
646  if (!empty($recipientData['realName'])) {
647  $recipient = [$recipientData['email'] => $recipientData['realName']];
648  } else {
649  $recipient = $recipientData['email'];
650  }
651  $mail->setTo($recipient)
652  ->setSubject($emailSubject)
653  ->setFrom(\TYPO3\CMS\Core\Utility\MailUtility::getSystemFrom())
654  ->setBody($emailMessage);
655  $mail->send();
656  }
657  $emailRecipients = implode(',', $emailRecipients);
658  $dataHandler->newlog2('Notification email for stage change was sent to "' . $emailRecipients . '"', $table, $id);
659  }
660  }
661 
670  protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = false)
671  {
672  $users = GeneralUtility::trimExplode(',', $listOfUsers, true);
673  $emails = [];
674  foreach ($users as $userIdent) {
675  if ($noTablePrefix) {
676  $id = (int)$userIdent;
677  } else {
678  list($table, $id) = GeneralUtility::revExplode('_', $userIdent, 2);
679  }
680  if ($table === 'be_users' || $noTablePrefix) {
681  if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) {
682  if (trim($userRecord['email']) !== '') {
683  $emails[$id] = $userRecord;
684  }
685  }
686  }
687  }
688  return $emails;
689  }
690 
691  /****************************
692  ***** Stage Changes ******
693  ****************************/
706  protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = false, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
707  {
708  if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
709  $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, 1);
710  } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) {
711  $record = BackendUtility::getRecord($table, $id);
712  $stat = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']);
713  // check if the usere is allowed to the current stage, so it's also allowed to send to next stage
714  if ($GLOBALS['BE_USER']->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
715  // Set stage of record:
716  GeneralUtility::makeInstance(ConnectionPool::class)
717  ->getConnectionForTable($table)
718  ->update(
719  $table,
720  [
721  't3ver_stage' => $stageId,
722  ],
723  ['uid' => (int)$id]
724  );
725  $dataHandler->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
726  // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
727  $dataHandler->log($table, $id, 6, 0, 0, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]);
728  if ((int)$stat['stagechg_notification'] > 0) {
730  $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$stat, $stageId, $comment];
731  $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id;
732  $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients;
733  } else {
734  $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients);
735  }
736  }
737  } else {
738  $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1);
739  }
740  } else {
741  $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', 1);
742  }
743  }
744 
745  /*****************************
746  ***** CMD versioning ******
747  *****************************/
748 
763  protected function version_swap($table, $id, $swapWith, $swapIntoWS = 0, DataHandler $dataHandler, $comment = '', $notificationEmailInfo = false, $notificationAlternativeRecipients = [])
764  {
765 
766  // Check prerequisites before start swapping
767 
768  // Skip records that have been deleted during the current execution
769  if ($dataHandler->hasDeletedRecord($table, $id)) {
770  return;
771  }
772 
773  // First, check if we may actually edit the online record
774  if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
775  $dataHandler->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1);
776  return;
777  }
778  // Select the two versions:
779  $curVersion = BackendUtility::getRecord($table, $id, '*');
780  $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
781  $movePlh = [];
782  $movePlhID = 0;
783  if (!(is_array($curVersion) && is_array($swapVersion))) {
784  $dataHandler->newlog('Error: Either online or swap version could not be selected!', 2);
785  return;
786  }
787  if (!$dataHandler->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
788  $dataHandler->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1);
789  return;
790  }
791  $wsAccess = $dataHandler->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']);
792  if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10)) {
793  $dataHandler->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1);
794  return;
795  }
796  if (!($dataHandler->doesRecordExist($table, $swapWith, 'show') && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) {
797  $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', 1);
798  return;
799  }
800  if ($swapIntoWS && !$dataHandler->BE_USER->workspaceSwapAccess()) {
801  $dataHandler->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1);
802  return;
803  }
804  // Check if the swapWith record really IS a version of the original!
805  if (!(((int)$swapVersion['pid'] == -1 && (int)$curVersion['pid'] >= 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
806  $dataHandler->newlog('In swap version, either pid was not -1 or the t3ver_oid didn\'t match the id of the online version as it must!', 2);
807  return;
808  }
809  // Lock file name:
810  $lockFileName = PATH_site . 'typo3temp/var/swap_locking/' . $table . '_' . $id . '.ser';
811  if (@is_file($lockFileName)) {
812  $dataHandler->newlog('A swapping lock file was present. Either another swap process is already running or a previous swap process failed. Ask your administrator to handle the situation.', 2);
813  return;
814  }
815 
816  // Now start to swap records by first creating the lock file
817 
818  // Write lock-file:
819  GeneralUtility::writeFileToTypo3tempDir($lockFileName, serialize([
820  'tstamp' => $GLOBALS['EXEC_TIME'],
821  'user' => $dataHandler->BE_USER->user['username'],
822  'curVersion' => $curVersion,
823  'swapVersion' => $swapVersion
824  ]));
825  // Find fields to keep
826  $keepFields = $this->getUniqueFields($table);
827  if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
828  $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
829  }
830  // l10n-fields must be kept otherwise the localization
831  // will be lost during the publishing
832  if ($table !== 'pages_language_overlay' && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
833  $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
834  }
835  // Swap "keepfields"
836  foreach ($keepFields as $fN) {
837  $tmp = $swapVersion[$fN];
838  $swapVersion[$fN] = $curVersion[$fN];
839  $curVersion[$fN] = $tmp;
840  }
841  // Preserve states:
842  $t3ver_state = [];
843  $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
844  $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
845  // Modify offline version to become online:
846  $tmp_wsid = $swapVersion['t3ver_wsid'];
847  // Set pid for ONLINE
848  $swapVersion['pid'] = (int)$curVersion['pid'];
849  // We clear this because t3ver_oid only make sense for offline versions
850  // and we want to prevent unintentional misuse of this
851  // value for online records.
852  $swapVersion['t3ver_oid'] = 0;
853  // In case of swapping and the offline record has a state
854  // (like 2 or 4 for deleting or move-pointer) we set the
855  // current workspace ID so the record is not deselected
856  // in the interface by BackendUtility::versioningPlaceholderClause()
857  $swapVersion['t3ver_wsid'] = 0;
858  if ($swapIntoWS) {
859  if ($t3ver_state['swapVersion'] > 0) {
860  $swapVersion['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
861  } else {
862  $swapVersion['t3ver_wsid'] = (int)$curVersion['t3ver_wsid'];
863  }
864  }
865  $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
866  $swapVersion['t3ver_stage'] = 0;
867  if (!$swapIntoWS) {
868  $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
869  }
870  // Moving element.
872  // && $t3ver_state['swapVersion']==4 // Maybe we don't need this?
873  if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) {
874  $movePlhID = $plhRec['uid'];
875  $movePlh['pid'] = $swapVersion['pid'];
876  $swapVersion['pid'] = (int)$plhRec['pid'];
877  $curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state'];
878  $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
879  if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
880  // sortby is a "keepFields" which is why this will work...
881  $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
882  $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
883  }
884  }
885  }
886  // Take care of relations in each field (e.g. IRRE):
887  if (is_array($GLOBALS['TCA'][$table]['columns'])) {
888  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
889  $this->version_swap_processFields($table, $field, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler);
890  }
891  }
892  unset($swapVersion['uid']);
893  // Modify online version to become offline:
894  unset($curVersion['uid']);
895  // Set pid for OFFLINE
896  $curVersion['pid'] = -1;
897  $curVersion['t3ver_oid'] = (int)$id;
898  $curVersion['t3ver_wsid'] = $swapIntoWS ? (int)$tmp_wsid : 0;
899  $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
900  $curVersion['t3ver_count'] = $curVersion['t3ver_count'] + 1;
901  // Increment lifecycle counter
902  $curVersion['t3ver_stage'] = 0;
903  if (!$swapIntoWS) {
904  $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
905  }
906  // Registering and swapping MM relations in current and swap records:
907  $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith);
908  // Generating proper history data to prepare logging
909  $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
910  $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
911 
912  // Execute swapping:
913  $sqlErrors = [];
914  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
915  $connection->update(
916  $table,
917  $swapVersion,
918  ['uid' => (int)$id]
919  );
920 
921  if ($connection->errorCode()) {
922  $sqlErrors[] = $connection->errorInfo();
923  } else {
924  $connection->update(
925  $table,
926  $curVersion,
927  ['uid' => (int)$swapWith]
928  );
929 
930  if ($connection->errorCode()) {
931  $sqlErrors[] = $connection->errorInfo();
932  } else {
933  unlink($lockFileName);
934  }
935  }
936  if (!empty($sqlErrors)) {
937  $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
938  } else {
939  // Register swapped ids for later remapping:
940  $this->remappedIds[$table][$id] = $swapWith;
941  $this->remappedIds[$table][$swapWith] = $id;
942  // If a moving operation took place...:
943  if ($movePlhID) {
944  // Remove, if normal publishing:
945  if (!$swapIntoWS) {
946  // For delete + completely delete!
947  $dataHandler->deleteEl($table, $movePlhID, true, true);
948  } else {
949  // Otherwise update the movePlaceholder:
950  GeneralUtility::makeInstance(ConnectionPool::class)
951  ->getConnectionForTable($table)
952  ->update(
953  $table,
954  $movePlh,
955  ['uid' => (int)$movePlhID]
956  );
957  $dataHandler->addRemapStackRefIndex($table, $movePlhID);
958  }
959  }
960  // Checking for delete:
961  // Delete only if new/deleted placeholders are there.
962  if (!$swapIntoWS && ((int)$t3ver_state['swapVersion'] === 1 || (int)$t3ver_state['swapVersion'] === 2)) {
963  // Force delete
964  $dataHandler->deleteEl($table, $id, true);
965  }
966  $dataHandler->newlog2(($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, $table, $id, $swapVersion['pid']);
967  // Update reference index of the live record:
968  $dataHandler->addRemapStackRefIndex($table, $id);
969  // Set log entry for live record:
970  $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion);
971  if ($propArr['_ORIG_pid'] == -1) {
972  $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
973  } else {
974  $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
975  }
976  $theLogId = $dataHandler->log($table, $id, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
977  $dataHandler->setHistory($table, $id, $theLogId);
978  // Update reference index of the offline record:
979  $dataHandler->addRemapStackRefIndex($table, $swapWith);
980  // Set log entry for offline record:
981  $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion);
982  if ($propArr['_ORIG_pid'] == -1) {
983  $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
984  } else {
985  $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
986  }
987  $theLogId = $dataHandler->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']);
988  $dataHandler->setHistory($table, $swapWith, $theLogId);
989 
990  $stageId = -20; // \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_EXECUTE_ID;
992  $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
993  $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
994  $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = $table . ':' . $id;
995  $this->notificationEmailInfo[$notificationEmailInfoKey]['alternativeRecipients'] = $notificationAlternativeRecipients;
996  } else {
997  $this->notifyStageChange($wsAccess, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients);
998  }
999  // Write to log with stageId -20
1000  $dataHandler->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
1001  $dataHandler->log($table, $id, 6, 0, 0, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
1002 
1003  // Clear cache:
1004  $dataHandler->registerRecordIdForPageCacheClearing($table, $id);
1005  // Checking for "new-placeholder" and if found, delete it (BUT FIRST after swapping!):
1006  if (!$swapIntoWS && $t3ver_state['curVersion'] > 0) {
1007  // For delete + completely delete!
1008  $dataHandler->deleteEl($table, $swapWith, true, true);
1009  }
1010 
1011  //Update reference index for live workspace too:
1013  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
1014  $refIndexObj->setWorkspaceId(0);
1015  $refIndexObj->updateRefIndexTable($table, $id);
1016  $refIndexObj->updateRefIndexTable($table, $swapWith);
1017  }
1018  }
1019 
1028  public function writeRemappedForeignField(\TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis, array $configuration, $parentId)
1029  {
1030  foreach ($dbAnalysis->itemArray as &$item) {
1031  if (isset($this->remappedIds[$item['table']][$item['id']])) {
1032  $item['id'] = $this->remappedIds[$item['table']][$item['id']];
1033  }
1034  }
1035  $dbAnalysis->writeForeignField($configuration, $parentId);
1036  }
1037 
1050  protected function version_swap_processFields($tableName, $fieldName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
1051  {
1052  $inlineType = $dataHandler->getInlineFieldType($configuration);
1053  if ($inlineType !== 'field') {
1054  return;
1055  }
1056  $foreignTable = $configuration['foreign_table'];
1057  // Read relations that point to the current record (e.g. live record):
1058  $liveRelations = $this->createRelationHandlerInstance();
1059  $liveRelations->setWorkspaceId(0);
1060  $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration);
1061  // Read relations that point to the record to be swapped with e.g. draft record):
1062  $versionRelations = $this->createRelationHandlerInstance();
1063  $versionRelations->setUseLiveReferenceIds(false);
1064  $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration);
1065  // Update relations for both (workspace/versioning) sites:
1066  if (count($liveRelations->itemArray)) {
1067  $dataHandler->addRemapAction(
1068  $tableName, $liveData['uid'],
1069  [$this, 'updateInlineForeignFieldSorting'],
1070  [$tableName, $liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace]
1071  );
1072  }
1073  if (count($versionRelations->itemArray)) {
1074  $dataHandler->addRemapAction(
1075  $tableName, $liveData['uid'],
1076  [$this, 'updateInlineForeignFieldSorting'],
1077  [$tableName, $liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0]
1078  );
1079  }
1080  }
1081 
1100  public function updateInlineForeignFieldSorting($parentTableName, $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
1101  {
1102  $remappedIds = [];
1103  // Use remapped ids (live id <-> version id)
1104  foreach ($foreignIds as $foreignId) {
1105  if (!empty($this->remappedIds[$foreignTableName][$foreignId])) {
1106  $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId];
1107  } else {
1108  $remappedIds[] = $foreignId;
1109  }
1110  }
1111 
1112  $relationHandler = $this->createRelationHandlerInstance();
1113  $relationHandler->setWorkspaceId($targetWorkspaceId);
1114  $relationHandler->setUseLiveReferenceIds(false);
1115  $relationHandler->start(implode(',', $remappedIds), $foreignTableName);
1116  $relationHandler->processDeletePlaceholder();
1117  $relationHandler->writeForeignField($configuration, $parentId);
1118  }
1119 
1129  protected function version_clearWSID($table, $id, $flush = false, DataHandler $dataHandler)
1130  {
1131  if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
1132  $dataHandler->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1);
1133  return;
1134  }
1135  if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
1136  $dataHandler->newlog('Attempt to reset workspace for record failed because you do not have edit access', 1);
1137  return;
1138  }
1139  $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
1140  if (!$liveRec) {
1141  return;
1142  }
1143  // Clear workspace ID:
1144  $updateData = [
1145  't3ver_wsid' => 0,
1146  't3ver_tstamp' => $GLOBALS['EXEC_TIME']
1147  ];
1148  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1149  $connection->update(
1150  $table,
1151  $updateData,
1152  ['uid' => (int)$id]
1153  );
1154 
1155  // Clear workspace ID for live version AND DELETE IT as well because it is a new record!
1156  if (
1157  VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1158  || VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1159  ) {
1160  $connection->update(
1161  $table,
1162  $updateData,
1163  ['uid' => (int)$liveRec['uid']]
1164  );
1165 
1166  // THIS assumes that the record was placeholder ONLY for ONE record (namely $id)
1167  $dataHandler->deleteEl($table, $liveRec['uid'], true);
1168  }
1169  // If "deleted" flag is set for the version that got released
1170  // it doesn't make sense to keep that "placeholder" anymore and we delete it completly.
1171  $wsRec = BackendUtility::getRecord($table, $id);
1172  if (
1173  $flush
1174  || (
1175  VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1176  || VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1177  )
1178  ) {
1179  $dataHandler->deleteEl($table, $id, true, true);
1180  }
1181  // Remove the move-placeholder if found for live record.
1183  if ($plhRec = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid')) {
1184  $dataHandler->deleteEl($table, $plhRec['uid'], true, true);
1185  }
1186  }
1187  }
1188 
1189  /*******************************
1190  ***** helper functions ******
1191  *******************************/
1192 
1201  public function findPageElementsForVersionSwap($table, $id, $offlineId)
1202  {
1203  $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid');
1204  $workspaceId = (int)$rec['t3ver_wsid'];
1205  $elementData = [];
1206  if ($workspaceId === 0) {
1207  return $elementData;
1208  }
1209  // Get page UID for LIVE and workspace
1210  if ($table != 'pages') {
1211  $rec = BackendUtility::getRecord($table, $id, 'pid');
1212  $pageId = $rec['pid'];
1213  $rec = BackendUtility::getRecord('pages', $pageId);
1214  BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1215  $offlinePageId = $rec['_ORIG_uid'];
1216  } else {
1217  $pageId = $id;
1218  $offlinePageId = $offlineId;
1219  }
1220  // Traversing all tables supporting versioning:
1221  foreach ($GLOBALS['TCA'] as $table => $cfg) {
1222  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') {
1223  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1224  ->getQueryBuilderForTable($table);
1225 
1226  $queryBuilder->getRestrictions()
1227  ->removeAll()
1228  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1229 
1230  $statement = $queryBuilder
1231  ->select('A.uid AS offlineUid', 'B.uid AS uid')
1232  ->from($table, 'A')
1233  ->from($table, 'B')
1234  ->where(
1235  $queryBuilder->expr()->eq(
1236  'A.pid',
1237  $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1238  ),
1239  $queryBuilder->expr()->eq(
1240  'B.pid',
1241  $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1242  ),
1243  $queryBuilder->expr()->eq(
1244  'A.t3ver_wsid',
1245  $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1246  ),
1247  $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1248  )
1249  ->execute();
1250 
1251  while ($row = $statement->fetch()) {
1252  $elementData[$table][] = [$row['uid'], $row['offlineUid']];
1253  }
1254  }
1255  }
1256  if ($offlinePageId && $offlinePageId != $pageId) {
1257  $elementData['pages'][] = [$pageId, $offlinePageId];
1258  }
1259 
1260  return $elementData;
1261  }
1262 
1271  public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList)
1272  {
1273  if ($workspaceId == 0) {
1274  return;
1275  }
1276  // Traversing all tables supporting versioning:
1277  foreach ($GLOBALS['TCA'] as $table => $cfg) {
1278  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') {
1279  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1280  ->getQueryBuilderForTable($table);
1281 
1282  $queryBuilder->getRestrictions()
1283  ->removeAll()
1284  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1285 
1286  $statement = $queryBuilder
1287  ->select('A.uid')
1288  ->from($table, 'A')
1289  ->from($table, 'B')
1290  ->where(
1291  $queryBuilder->expr()->eq(
1292  'A.pid',
1293  $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1294  ),
1295  $queryBuilder->expr()->in(
1296  'B.pid',
1297  $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY)
1298  ),
1299  $queryBuilder->expr()->eq(
1300  'A.t3ver_wsid',
1301  $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1302  ),
1303  $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1304  )
1305  ->groupBy('A.uid')
1306  ->execute();
1307 
1308  while ($row = $statement->fetch()) {
1309  $elementList[$table][] = $row['uid'];
1310  }
1311  if (is_array($elementList[$table])) {
1312  // Yes, it is possible to get non-unique array even with DISTINCT above!
1313  // It happens because several UIDs are passed in the array already.
1314  $elementList[$table] = array_unique($elementList[$table]);
1315  }
1316  }
1317  }
1318  }
1319 
1330  public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList)
1331  {
1332  if ($workspaceId == 0) {
1333  return;
1334  }
1335 
1336  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1337  ->getQueryBuilderForTable($table);
1338  $queryBuilder->getRestrictions()
1339  ->removeAll()
1340  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1341 
1342  $statement = $queryBuilder
1343  ->select('B.pid')
1344  ->from($table, 'A')
1345  ->from($table, 'B')
1346  ->where(
1347  $queryBuilder->expr()->eq(
1348  'A.pid',
1349  $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1350  ),
1351  $queryBuilder->expr()->eq(
1352  'A.t3ver_wsid',
1353  $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1354  ),
1355  $queryBuilder->expr()->in(
1356  'A.uid',
1357  $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
1358  ),
1359  $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1360  )
1361  ->groupBy('B.pid')
1362  ->execute();
1363 
1364  while ($row = $statement->fetch()) {
1365  $pageIdList[] = $row['pid'];
1366  // Find ws version
1367  // Note: cannot use BackendUtility::getRecordWSOL()
1368  // here because it does not accept workspace id!
1369  $rec = BackendUtility::getRecord('pages', $row[0]);
1370  BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1371  if ($rec['_ORIG_uid']) {
1372  $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1373  }
1374  }
1375  // The line below is necessary even with DISTINCT
1376  // because several elements can be passed by caller
1377  $pageIdList = array_unique($pageIdList);
1378  }
1379 
1386  public function findRealPageIds(array &$idList)
1387  {
1388  foreach ($idList as $key => $id) {
1389  $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid');
1390  if ($rec['t3ver_oid'] > 0) {
1391  $idList[$key] = $rec['t3ver_oid'];
1392  }
1393  }
1394  }
1395 
1410  protected function moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $dataHandler)
1411  {
1412  // If a record gets moved after a record that already has a placeholder record
1413  // then the new placeholder record needs to be after the existing one
1414  $originalRecordDestinationPid = $destPid;
1415  if ($destPid < 0) {
1416  $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid');
1417  if ($movePlaceHolder !== false) {
1418  $destPid = -$movePlaceHolder['uid'];
1419  }
1420  }
1421  if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) {
1422  // If already a placeholder exists, move it:
1423  $dataHandler->moveRecord_raw($table, $plh['uid'], $destPid);
1424  } else {
1425  // First, we create a placeholder record in the Live workspace that
1426  // represents the position to where the record is eventually moved to.
1427  $newVersion_placeholderFieldArray = [];
1428 
1429  // Use property for move placeholders if set (since TYPO3 CMS 6.2)
1430  if (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'])) {
1431  $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'];
1432  // Fallback to property for new placeholder (existed long time before TYPO3 CMS 6.2)
1433  } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'])) {
1434  $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1435  }
1436 
1437  // Set values from the versioned record to the move placeholder
1438  if (!empty($shadowColumnsForMovePlaceholder)) {
1439  $versionedRecord = BackendUtility::getRecord($table, $wsUid);
1440  $shadowColumns = GeneralUtility::trimExplode(',', $shadowColumnsForMovePlaceholder, true);
1441  foreach ($shadowColumns as $shadowColumn) {
1442  if (isset($versionedRecord[$shadowColumn])) {
1443  $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn];
1444  }
1445  }
1446  }
1447 
1448  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1449  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1450  }
1451  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1452  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $dataHandler->userid;
1453  }
1454  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
1455  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1456  }
1457  if ($table == 'pages') {
1458  // Copy page access settings from original page to placeholder
1459  $perms_clause = $dataHandler->BE_USER->getPagePermsClause(1);
1460  $access = BackendUtility::readPageAccess($uid, $perms_clause);
1461  $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid'];
1462  $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid'];
1463  $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user'];
1464  $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group'];
1465  $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody'];
1466  }
1467  $newVersion_placeholderFieldArray['t3ver_label'] = 'MovePlaceholder #' . $uid;
1468  $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid;
1469  // Setting placeholder state value for temporary record
1470  $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
1471  // Setting workspace - only so display of place holders can filter out those from other workspaces.
1472  $newVersion_placeholderFieldArray['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
1473  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $dataHandler->getPlaceholderTitleForTableLabel($table, 'MOVE-TO PLACEHOLDER for #' . $uid);
1474  // moving localized records requires to keep localization-settings for the placeholder too
1475  if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
1476  $l10nParentRec = BackendUtility::getRecord($table, $uid);
1477  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
1478  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
1479  if (isset($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])) {
1480  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']];
1481  }
1482  unset($l10nParentRec);
1483  }
1484  // Initially, create at root level.
1485  $newVersion_placeholderFieldArray['pid'] = 0;
1486  $id = 'NEW_MOVE_PLH';
1487  // Saving placeholder as 'original'
1488  $dataHandler->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1489  // Move the new placeholder from temporary root-level to location:
1490  $dataHandler->moveRecord_raw($table, $dataHandler->substNEWwithIDs[$id], $destPid);
1491  // Move the workspace-version of the original to be the version of the move-to-placeholder:
1492  // Setting placeholder state value for version (so it can know it is currently a new version...)
1493  $updateFields = [
1494  't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER)
1495  ];
1496 
1497  GeneralUtility::makeInstance(ConnectionPool::class)
1498  ->getConnectionForTable($table)
1499  ->update(
1500  $table,
1501  $updateFields,
1502  ['uid' => (int)$wsUid]
1503  );
1504  }
1505  // Check for the localizations of that element and move them as well
1506  $dataHandler->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
1507  }
1508 
1515  public function getCommandMap(DataHandler $dataHandler)
1516  {
1518  \TYPO3\CMS\Version\DataHandler\CommandMap::class,
1519  $this,
1520  $dataHandler,
1521  $dataHandler->cmdmap,
1522  $dataHandler->BE_USER->workspace
1523  );
1524  }
1525 
1532  protected function getUniqueFields($table)
1533  {
1534  $listArr = [];
1535  if (empty($GLOBALS['TCA'][$table]['columns'])) {
1536  return $listArr;
1537  }
1538  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) {
1539  if ($configArr['config']['type'] === 'input') {
1540  $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], true);
1541  if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) {
1542  $listArr[] = $field;
1543  }
1544  }
1545  }
1546  return $listArr;
1547  }
1548 
1552  protected function createRelationHandlerInstance()
1553  {
1554  return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\RelationHandler::class);
1555  }
1556 
1560  protected function getLanguageService()
1561  {
1562  return $GLOBALS['LANG'];
1563  }
1564 }
static getRecordPath($uid, $clause, $titleLimit, $fullTitleLimit=0)
writeRemappedForeignField(\TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis, array $configuration, $parentId)
moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $dataHandler)
findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList)
updateInlineForeignFieldSorting($parentTableName, $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
static isFirstPartOfStr($str, $partStr)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
log($table, $recuid, $action, $recpid, $error, $details, $details_nr=-1, $data=[], $event_pid=-1, $NEWid= '')
static arrayDiffAssocRecursive(array $array1, array $array2)
checkRecordUpdateAccess($table, $id, $data=false, $hookObjectsArr=null)
processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
newlog2($message, $table, $uid, $pid=null, $error=0)
static getMovePlaceholder($table, $uid, $fields= '*', $workspace=null)
moveRecord_raw($table, $uid, $destPid)
moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
version_swap_processFields($tableName, $fieldName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
static getRecordTitle($table, $row, $prep=false, $forceResult=true)
processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
static workspaceOL($table, &$row, $wsid=-99, $unsetMovePointers=false)
moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration)
static getRecord($table, $uid, $fields= '*', $where= '', $useDeleteClause=true)
version_setStage($table, $id, $stageId, $comment= '', $notificationEmailInfo=false, DataHandler $dataHandler, array $notificationAlternativeRecipients=[])
insertDB($table, $id, $fieldArray, $newVersion=false, $suggestedUid=0, $dontSetNewIdIndex=false)
compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList)
deleteEl($table, $uid, $noRecordCheck=false, $forceHardDelete=false)
static writeFileToTypo3tempDir($filepath, $content)
static getPagesTSconfig($id, $rootLine=null, $returnPartArray=false)
versionizeRecord($table, $id, $label, $delete=false)
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)
version_clearWSID($table, $id, $flush=false, DataHandler $dataHandler)
getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix=false)
addRemapAction($table, $id, array $callback, array $arguments)
moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
static getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields= '*')
registerRecordIdForPageCacheClearing($table, $uid, $pid=null)
static revExplode($delimiter, $string, $count=0)
static fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch=false)
getPlaceholderTitleForTableLabel($table, $placeholderContent=null)