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