TYPO3 CMS  TYPO3_7-6
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 
24 
30 {
38  protected $notificationEmailInfo = [];
39 
45  protected $remappedIds = [];
46 
47  /****************************
48  ***** Cmdmap Hooks ******
49  ****************************/
56  public function processCmdmap_beforeStart(DataHandler $tcemainObj)
57  {
58  // Reset notification array
59  $this->notificationEmailInfo = [];
60  // Resolve dependencies of version/workspaces actions:
61  $tcemainObj->cmdmap = $this->getCommandMap($tcemainObj)->process()->get();
62  }
63 
75  public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $tcemainObj)
76  {
77  // custom command "version"
78  if ($command == 'version') {
79  $commandIsProcessed = true;
80  $action = (string)$value['action'];
81  $comment = !empty($value['comment']) ? $value['comment'] : '';
82  $notificationAlternativeRecipients = (isset($value['notificationAlternativeRecipients'])) && is_array($value['notificationAlternativeRecipients']) ? $value['notificationAlternativeRecipients'] : [];
83  switch ($action) {
84  case 'new':
85  // check if page / branch versioning is needed,
86  // or if "element" version can be used
87  $versionizeTree = -1;
88  if (isset($value['treeLevels'])) {
89  $versionizeTree = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($value['treeLevels'], -1, 100);
90  }
91  if ($table == 'pages' && $versionizeTree >= 0) {
92  $this->versionizePages($id, $value['label'], $versionizeTree, $tcemainObj);
93  } else {
94  $tcemainObj->versionizeRecord($table, $id, $value['label']);
95  }
96  break;
97  case 'swap':
98  $this->version_swap($table, $id, $value['swapWith'], $value['swapIntoWS'],
99  $tcemainObj,
100  $comment,
101  true,
102  $notificationAlternativeRecipients
103  );
104  break;
105  case 'clearWSID':
106  $this->version_clearWSID($table, $id, false, $tcemainObj);
107  break;
108  case 'flush':
109  $this->version_clearWSID($table, $id, true, $tcemainObj);
110  break;
111  case 'setStage':
112  $elementIds = GeneralUtility::trimExplode(',', $id, true);
113  foreach ($elementIds as $elementId) {
114  $this->version_setStage($table, $elementId, $value['stageId'],
115  $comment,
116  true,
117  $tcemainObj,
118  $notificationAlternativeRecipients
119  );
120  }
121  break;
122  default:
123  // Do nothing
124  }
125  }
126  }
127 
135  public function processCmdmap_afterFinish(DataHandler $tcemainObj)
136  {
137  // Empty accumulation array:
138  foreach ($this->notificationEmailInfo as $notifItem) {
139  $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $tcemainObj, $notifItem['alternativeRecipients']);
140  }
141  // Reset notification array
142  $this->notificationEmailInfo = [];
143  // Reset remapped IDs
144  $this->remappedIds = [];
145  }
146 
157  public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $tcemainObj)
158  {
159  // only process the hook if it wasn't processed
160  // by someone else before
161  if ($recordWasDeleted) {
162  return;
163  }
164  $recordWasDeleted = true;
165  // For Live version, try if there is a workspace version because if so, rather "delete" that instead
166  // Look, if record is an offline version, then delete directly:
167  if ($record['pid'] != -1) {
168  if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($tcemainObj->BE_USER->workspace, $table, $id)) {
169  $record = $wsVersion;
170  $id = $record['uid'];
171  }
172  }
173  $recordVersionState = VersionState::cast($record['t3ver_state']);
174  // Look, if record is an offline version, then delete directly:
175  if ($record['pid'] == -1) {
176  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
177  // In Live workspace, delete any. In other workspaces there must be match.
178  if ($tcemainObj->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $tcemainObj->BE_USER->workspace) {
179  $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
180  // Processing can be skipped if a delete placeholder shall be swapped/published
181  // during the current request. Thus it will be deleted later on...
182  $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']);
183  if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid'])
184  && !empty($tcemainObj->cmdmap[$table][$liveRec['uid']]['version']['action'])
185  && !empty($tcemainObj->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
186  && $tcemainObj->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
187  && $tcemainObj->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
188  ) {
189  return null;
190  }
191 
192  if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) {
193  // Change normal versioned record to delete placeholder
194  // Happens when an edited record is deleted
195  $updateFields = [
196  't3ver_label' => 'DELETED!',
197  't3ver_state' => 2,
198  ];
199  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . $id, $updateFields);
200  // Delete localization overlays:
201  $tcemainObj->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  $tcemainObj->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  $tcemainObj->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, $tcemainObj);
216  }
217  } else {
218  $tcemainObj->newlog('Tried to delete record from another workspace', 1);
219  }
220  } else {
221  $tcemainObj->newlog('Versioning not enabled for record with PID = -1!', 2);
222  }
223  } elseif ($res = $tcemainObj->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  $tcemainObj->deleteEl($table, $id);
227  } else {
228  $tcemainObj->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  $updateFields = [
237  't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE)
238  ];
239  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$wsRec['uid'], $updateFields);
240  }
241  $tcemainObj->deleteEl($table, $id);
242  } else {
243  // Otherwise, try to delete by versioning:
244  $copyMappingArray = $tcemainObj->copyMappingArray;
245  $tcemainObj->versionizeRecord($table, $id, 'DELETED!', true);
246  // Determine newly created versions:
247  // (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
248  $versionizedElements = ArrayUtility::arrayDiffAssocRecursive($tcemainObj->copyMappingArray, $copyMappingArray);
249  // Delete localization overlays:
250  foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
251  foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
252  $tcemainObj->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
253  }
254  }
255  }
256  }
257 
272  public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $tcemainObj)
273  {
274  // Only do something in Draft workspace
275  if ($tcemainObj->BE_USER->workspace === 0) {
276  return;
277  }
278  if ($destPid < 0) {
279  // Fetch move placeholder, since it might point to a new page in the current workspace
280  $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid');
281  if ($movePlaceHolder !== false) {
282  $resolvedPid = $movePlaceHolder['pid'];
283  }
284  }
285  $recordWasMoved = true;
286  $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']);
287  // Get workspace version of the source record, if any:
288  $WSversion = BackendUtility::getWorkspaceVersionOfRecord($tcemainObj->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
289  // Handle move-placeholders if the current record is not one already
290  if (
292  && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER)
293  ) {
294  // Create version of record first, if it does not exist
295  if (empty($WSversion['uid'])) {
296  $tcemainObj->versionizeRecord($table, $uid, 'MovePointer');
297  $WSversion = BackendUtility::getWorkspaceVersionOfRecord($tcemainObj->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
298  $this->moveRecord_processFields($tcemainObj, $resolvedPid, $table, $uid);
299  // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
300  } elseif ($tcemainObj->isRecordCopied($table, $uid) && (int)$tcemainObj->copyMappingArray[$table][$uid] === (int)$WSversion['uid']) {
301  $this->moveRecord_processFields($tcemainObj, $resolvedPid, $table, $uid);
302  }
303  }
304  // Check workspace permissions:
305  $workspaceAccessBlocked = [];
306  // Element was in "New/Deleted/Moved" so it can be moved...
307  $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder();
308  $destRes = $tcemainObj->BE_USER->workspaceAllowLiveRecordsInPID($resolvedPid, $table);
309  $canMoveRecord = ($recIsNewVersion || BackendUtility::isTableWorkspaceEnabled($table));
310  // Workspace source check:
311  if (!$recIsNewVersion) {
312  $errorCode = $tcemainObj->BE_USER->workspaceCannotEditRecord($table, $WSversion['uid'] ? $WSversion['uid'] : $uid);
313  if ($errorCode) {
314  $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
315  } elseif (!$canMoveRecord && $tcemainObj->BE_USER->workspaceAllowLiveRecordsInPID($moveRec['pid'], $table) <= 0) {
316  $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
317  }
318  }
319  // Workspace destination check:
320  // All records can be inserted if $destRes is greater than zero.
321  // Only new versions can be inserted if $destRes is FALSE.
322  // NO RECORDS can be inserted if $destRes is negative which indicates a stage
323  // not allowed for use. If "versioningWS" is version 2, moving can take place of versions.
324  // since TYPO3 CMS 7, version2 is the default and the only option
325  if (!($destRes > 0 || $canMoveRecord && !$destRes)) {
326  $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
327  } elseif ($destRes == 1 && $WSversion['uid']) {
328  $workspaceAccessBlocked['dest2'] = 'Could not insert other versions in destination PID ';
329  }
330  if (empty($workspaceAccessBlocked)) {
331  // If the move operation is done on a versioned record, which is
332  // NOT new/deleted placeholder and versioningWS is in version 2, then...
333  // since TYPO3 CMS 7, version2 is the default and the only option
334  if ($WSversion['uid'] && !$recIsNewVersion && BackendUtility::isTableWorkspaceEnabled($table)) {
335  $this->moveRecord_wsPlaceholders($table, $uid, $destPid, $WSversion['uid'], $tcemainObj);
336  } else {
337  // moving not needed, just behave like in live workspace
338  $recordWasMoved = false;
339  }
340  } else {
341  $tcemainObj->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), 1);
342  }
343  }
344 
354  protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
355  {
356  $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
357  if (empty($versionedRecord)) {
358  return;
359  }
360  foreach ($versionedRecord as $field => $value) {
361  if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
362  continue;
363  }
365  $dataHandler, $resolvedPageId,
366  $table, $uid, $field, $value,
367  $GLOBALS['TCA'][$table]['columns'][$field]['config']
368  );
369  }
370  }
371 
384  protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration)
385  {
386  $inlineFieldType = $dataHandler->getInlineFieldType($configuration);
387  $inlineProcessing = (
388  ($inlineFieldType === 'list' || $inlineFieldType === 'field')
389  && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
390  && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
391  );
392 
393  if ($inlineProcessing) {
394  if ($table === 'pages') {
395  // If the inline elements are related to a page record,
396  // make sure they reside at that page and not at its parent
397  $resolvedPageId = $uid;
398  }
399 
400  $dbAnalysis = $this->createRelationHandlerInstance();
401  $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
402 
403  // Moving records to a positive destination will insert each
404  // record at the beginning, thus the order is reversed here:
405  foreach ($dbAnalysis->itemArray as $item) {
406  $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
407  if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) {
408  continue;
409  }
410  $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
411  }
412  }
413  }
414 
415  /****************************
416  ***** Notifications ******
417  ****************************/
430  protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $tcemainObj, array $notificationAlternativeRecipients = [])
431  {
432  $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
433  // So, if $id is not set, then $table is taken to be the complete element name!
434  $elementName = $id ? $table . ':' . $id : $table;
435  if (!is_array($workspaceRec)) {
436  return;
437  }
438 
439  // Get the new stage title from workspaces library, if workspaces extension is installed
440  if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) {
441  $stageService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\StagesService::class);
442  $newStage = $stageService->getStageTitle((int)$stageId);
443  } else {
444  // @todo CONSTANTS SHOULD BE USED - tx_service_workspace_workspaces
445  // @todo use localized labels
446  // Compile label:
447  switch ((int)$stageId) {
448  case 1:
449  $newStage = 'Ready for review';
450  break;
451  case 10:
452  $newStage = 'Ready for publishing';
453  break;
454  case -1:
455  $newStage = 'Element was rejected!';
456  break;
457  case 0:
458  $newStage = 'Rejected element was noticed and edited';
459  break;
460  default:
461  $newStage = 'Unknown state change!?';
462  }
463  }
464  if (empty($notificationAlternativeRecipients)) {
465  // Compile list of recipients:
466  $emails = [];
467  switch ((int)$stat['stagechg_notification']) {
468  case 1:
469  switch ((int)$stageId) {
470  case 1:
471  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
472  break;
473  case 10:
474  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
475  break;
476  case -1:
477  // List of elements to reject:
478  $allElements = explode(',', $elementName);
479  // Traverse them, and find the history of each
480  foreach ($allElements as $elRef) {
481  list($eTable, $eUid) = explode(':', $elRef);
482  $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('log_data,tstamp,userid', 'sys_log', 'action=6 and details_nr=30
483  AND tablename=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($eTable, 'sys_log') . '
484  AND recuid=' . (int)$eUid, '', 'uid DESC');
485  // Find all implicated since the last stage-raise from editing to review:
486  foreach ($rows as $dat) {
487  $data = unserialize($dat['log_data']);
488  $emails = $this->getEmailsForStageChangeNotification($dat['userid'], true) + $emails;
489  if ($data['stage'] == 1) {
490  break;
491  }
492  }
493  }
494  break;
495  case 0:
496  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
497  break;
498  default:
499  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
500  }
501  break;
502  case 10:
503  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
504  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']) + $emails;
505  $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails;
506  break;
507  default:
508  // Do nothing
509  }
510  } else {
511  $emails = $notificationAlternativeRecipients;
512  }
513  // prepare and then send the emails
514  if (!empty($emails)) {
515  // Path to record is found:
516  list($elementTable, $elementUid) = explode(':', $elementName);
517  $elementUid = (int)$elementUid;
518  $elementRecord = BackendUtility::getRecord($elementTable, $elementUid);
519  $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord);
520  if ($elementTable == 'pages') {
521  $pageUid = $elementUid;
522  } else {
523  BackendUtility::fixVersioningPid($elementTable, $elementRecord);
524  $pageUid = ($elementUid = $elementRecord['pid']);
525  }
526  // fetch the TSconfig settings for the email
527  // old way, options are TCEMAIN.notificationEmail_body/subject
528  $TCEmainTSConfig = $tcemainObj->getTCEMAIN_TSconfig($pageUid);
529  // new way, options are
530  // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject
531  // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject
532  $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid);
533  $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.'];
534  $markers = [
535  '###RECORD_TITLE###' => $recordTitle,
536  '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20),
537  '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
538  '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir,
539  '###WORKSPACE_TITLE###' => $workspaceRec['title'],
540  '###WORKSPACE_UID###' => $workspaceRec['uid'],
541  '###ELEMENT_NAME###' => $elementName,
542  '###NEXT_STAGE###' => $newStage,
543  '###COMMENT###' => $comment,
544  // See: #30212 - keep both markers for compatibility
545  '###USER_REALNAME###' => $tcemainObj->BE_USER->user['realName'],
546  '###USER_FULLNAME###' => $tcemainObj->BE_USER->user['realName'],
547  '###USER_USERNAME###' => $tcemainObj->BE_USER->user['username']
548  ];
549  // add marker for preview links if workspace extension is loaded
550  if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) {
551  $this->workspaceService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class);
552  // only generate the link if the marker is in the template - prevents database from getting to much entries
553  if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) {
554  $tempEmailMessage = $GLOBALS['LANG']->sL($emailConfig['message']);
555  } else {
556  $tempEmailMessage = $emailConfig['message'];
557  }
558  if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== false) {
559  $markers['###PREVIEW_LINK###'] = $this->workspaceService->generateWorkspacePreviewLink($elementUid);
560  }
561  unset($tempEmailMessage);
562  $markers['###SPLITTED_PREVIEW_LINK###'] = $this->workspaceService->generateWorkspaceSplittedPreviewLink($elementUid, true);
563  }
564  // Hook for preprocessing of the content for formmails:
565  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'])) {
566  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] as $_classRef) {
567  $_procObj =& GeneralUtility::getUserObj($_classRef);
568  $markers = $_procObj->postModifyMarkers($markers, $this);
569  }
570  }
571  // send an email to each individual user, to ensure the
572  // multilanguage version of the email
573  $emailRecipients = [];
574  // an array of language objects that are needed
575  // for emails with different languages
576  $languageObjects = [
577  $GLOBALS['LANG']->lang => $GLOBALS['LANG']
578  ];
579  // loop through each recipient and send the email
580  foreach ($emails as $recipientData) {
581  // don't send an email twice
582  if (isset($emailRecipients[$recipientData['email']])) {
583  continue;
584  }
585  $emailSubject = $emailConfig['subject'];
586  $emailMessage = $emailConfig['message'];
587  $emailRecipients[$recipientData['email']] = $recipientData['email'];
588  // check if the email needs to be localized
589  // in the users' language
590  if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
591  $recipientLanguage = $recipientData['lang'] ? $recipientData['lang'] : 'default';
592  if (!isset($languageObjects[$recipientLanguage])) {
593  // a LANG object in this language hasn't been
594  // instantiated yet, so this is done here
596  $languageObject = GeneralUtility::makeInstance(\TYPO3\CMS\Lang\LanguageService::class);
597  $languageObject->init($recipientLanguage);
598  $languageObjects[$recipientLanguage] = $languageObject;
599  } else {
600  $languageObject = $languageObjects[$recipientLanguage];
601  }
602  if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) {
603  $emailSubject = $languageObject->sL($emailSubject);
604  }
605  if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
606  $emailMessage = $languageObject->sL($emailMessage);
607  }
608  }
609  $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
610  $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true);
611  $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true);
612  // Send an email to the recipient
614  $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
615  if (!empty($recipientData['realName'])) {
616  $recipient = [$recipientData['email'] => $recipientData['realName']];
617  } else {
618  $recipient = $recipientData['email'];
619  }
620  $mail->setTo($recipient)
621  ->setSubject($emailSubject)
622  ->setFrom(\TYPO3\CMS\Core\Utility\MailUtility::getSystemFrom())
623  ->setBody($emailMessage);
624  $mail->send();
625  }
626  $emailRecipients = implode(',', $emailRecipients);
627  $tcemainObj->newlog2('Notification email for stage change was sent to "' . $emailRecipients . '"', $table, $id);
628  }
629  }
630 
639  protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = false)
640  {
641  $users = GeneralUtility::trimExplode(',', $listOfUsers, true);
642  $emails = [];
643  foreach ($users as $userIdent) {
644  if ($noTablePrefix) {
645  $id = (int)$userIdent;
646  } else {
647  list($table, $id) = GeneralUtility::revExplode('_', $userIdent, 2);
648  }
649  if ($table === 'be_users' || $noTablePrefix) {
650  if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) {
651  if (trim($userRecord['email']) !== '') {
652  $emails[$id] = $userRecord;
653  }
654  }
655  }
656  }
657  return $emails;
658  }
659 
660  /****************************
661  ***** Stage Changes ******
662  ****************************/
675  protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = false, DataHandler $tcemainObj, array $notificationAlternativeRecipients = [])
676  {
677  if ($errorCode = $tcemainObj->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
678  $tcemainObj->newlog('Attempt to set stage for record failed: ' . $errorCode, 1);
679  } elseif ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
680  $record = BackendUtility::getRecord($table, $id);
681  $stat = $tcemainObj->BE_USER->checkWorkspace($record['t3ver_wsid']);
682  // check if the usere is allowed to the current stage, so it's also allowed to send to next stage
683  if ($GLOBALS['BE_USER']->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
684  // Set stage of record:
685  $updateData = [
686  't3ver_stage' => $stageId
687  ];
688  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$id, $updateData);
689  $tcemainObj->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
690  // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
691  $tcemainObj->log($table, $id, 6, 0, 0, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]);
692  if ((int)$stat['stagechg_notification'] > 0) {
694  $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$stat, $stageId, $comment];
695  $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id;
696  $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients;
697  } else {
698  $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $tcemainObj, $notificationAlternativeRecipients);
699  }
700  }
701  } else {
702  $tcemainObj->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1);
703  }
704  } else {
705  $tcemainObj->newlog('Attempt to set stage for record failed because you do not have edit access', 1);
706  }
707  }
708 
709  /*****************************
710  ***** CMD versioning ******
711  *****************************/
722  protected function versionizePages($uid, $label, $versionizeTree, DataHandler $tcemainObj)
723  {
724  $uid = (int)$uid;
725  // returns the branch
726  $brExist = $tcemainObj->doesBranchExist('', $uid, $tcemainObj->pMap['show'], 1);
727  // Checks if we had permissions
728  if ((int)$brExist === -1) {
729  $tcemainObj->newlog('Could not read all subpages to versionize.', 1);
730  return;
731  }
732  // Make list of tables that should come along with a new version of the page:
733  $verTablesArray = [];
734  $allTables = array_keys($GLOBALS['TCA']);
735  foreach ($allTables as $tableName) {
736  if ($tableName != 'pages' && ($versionizeTree > 0 || $GLOBALS['TCA'][$tableName]['ctrl']['versioning_followPages'])) {
737  $verTablesArray[] = $tableName;
738  }
739  }
740  // Remove the possible inline child tables from the tables to be versioniozed automatically:
741  $verTablesArray = array_diff($verTablesArray, $this->getPossibleInlineChildTablesOfParentTable('pages'));
742  // Begin to copy pages if we're allowed to:
743  if ($versionizeTree !== -1) {
744  $tcemainObj->newlog('Versioning type "' . $versionizeTree . '" was not allowed in workspace', 1);
745  return;
746  }
747  // Versionize this page:
748  $theNewRootID = $tcemainObj->versionizeRecord('pages', $uid, $label, false, $versionizeTree);
749  if (!$theNewRootID) {
750  $tcemainObj->newlog('The root version could not be created!', 1);
751  return;
752  }
753  $this->rawCopyPageContent($uid, $theNewRootID, $verTablesArray, $tcemainObj);
754  // If we're going to copy recursively...:
755  if ($versionizeTree > 0) {
756  // Get ALL subpages to copy (read permissions respected - they should NOT be...):
757  $CPtable = $tcemainObj->int_pageTreeInfo([], $uid, (int)$versionizeTree, $theNewRootID);
758  // Now copying the subpages
759  foreach ($CPtable as $thePageUid => $thePagePid) {
760  $newPid = $tcemainObj->copyMappingArray['pages'][$thePagePid];
761  if (isset($newPid)) {
762  $theNewRootID = $tcemainObj->copyRecord_raw('pages', $thePageUid, $newPid);
763  $this->rawCopyPageContent($thePageUid, $theNewRootID, $verTablesArray, $tcemainObj);
764  } else {
765  $tcemainObj->newlog('Something went wrong during copying branch (for versioning)', 1);
766  break;
767  }
768  }
769  }
770  }
771 
786  protected function version_swap($table, $id, $swapWith, $swapIntoWS = 0, DataHandler $tcemainObj, $comment = '', $notificationEmailInfo = false, $notificationAlternativeRecipients = [])
787  {
788 
789  // Check prerequisites before start swapping
790 
791  // Skip records that have been deleted during the current execution
792  if ($tcemainObj->hasDeletedRecord($table, $id)) {
793  return;
794  }
795 
796  // First, check if we may actually edit the online record
797  if (!$tcemainObj->checkRecordUpdateAccess($table, $id)) {
798  $tcemainObj->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1);
799  return;
800  }
801  // Select the two versions:
802  $curVersion = BackendUtility::getRecord($table, $id, '*');
803  $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
804  $movePlh = [];
805  $movePlhID = 0;
806  if (!(is_array($curVersion) && is_array($swapVersion))) {
807  $tcemainObj->newlog('Error: Either online or swap version could not be selected!', 2);
808  return;
809  }
810  if (!$tcemainObj->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
811  $tcemainObj->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1);
812  return;
813  }
814  $wsAccess = $tcemainObj->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']);
815  if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10)) {
816  $tcemainObj->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1);
817  return;
818  }
819  if (!($tcemainObj->doesRecordExist($table, $swapWith, 'show') && $tcemainObj->checkRecordUpdateAccess($table, $swapWith))) {
820  $tcemainObj->newlog('You cannot publish a record you do not have edit and show permissions for', 1);
821  return;
822  }
823  if ($swapIntoWS && !$tcemainObj->BE_USER->workspaceSwapAccess()) {
824  $tcemainObj->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1);
825  return;
826  }
827  // Check if the swapWith record really IS a version of the original!
828  if (!(((int)$swapVersion['pid'] == -1 && (int)$curVersion['pid'] >= 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
829  $tcemainObj->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);
830  return;
831  }
832  // Lock file name:
833  $lockFileName = PATH_site . 'typo3temp/swap_locking/' . $table . '_' . $id . '.ser';
834  if (@is_file($lockFileName)) {
835  $tcemainObj->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);
836  return;
837  }
838 
839  // Now start to swap records by first creating the lock file
840 
841  // Write lock-file:
842  GeneralUtility::writeFileToTypo3tempDir($lockFileName, serialize([
843  'tstamp' => $GLOBALS['EXEC_TIME'],
844  'user' => $tcemainObj->BE_USER->user['username'],
845  'curVersion' => $curVersion,
846  'swapVersion' => $swapVersion
847  ]));
848  // Find fields to keep
849  $keepFields = $this->getUniqueFields($table);
850  if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
851  $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
852  }
853  // l10n-fields must be kept otherwise the localization
854  // will be lost during the publishing
855  if (!isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']) && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
856  $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
857  }
858  // Swap "keepfields"
859  foreach ($keepFields as $fN) {
860  $tmp = $swapVersion[$fN];
861  $swapVersion[$fN] = $curVersion[$fN];
862  $curVersion[$fN] = $tmp;
863  }
864  // Preserve states:
865  $t3ver_state = [];
866  $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
867  $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
868  // Modify offline version to become online:
869  $tmp_wsid = $swapVersion['t3ver_wsid'];
870  // Set pid for ONLINE
871  $swapVersion['pid'] = (int)$curVersion['pid'];
872  // We clear this because t3ver_oid only make sense for offline versions
873  // and we want to prevent unintentional misuse of this
874  // value for online records.
875  $swapVersion['t3ver_oid'] = 0;
876  // In case of swapping and the offline record has a state
877  // (like 2 or 4 for deleting or move-pointer) we set the
878  // current workspace ID so the record is not deselected
879  // in the interface by BackendUtility::versioningPlaceholderClause()
880  $swapVersion['t3ver_wsid'] = 0;
881  if ($swapIntoWS) {
882  if ($t3ver_state['swapVersion'] > 0) {
883  $swapVersion['t3ver_wsid'] = $tcemainObj->BE_USER->workspace;
884  } else {
885  $swapVersion['t3ver_wsid'] = (int)$curVersion['t3ver_wsid'];
886  }
887  }
888  $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
889  $swapVersion['t3ver_stage'] = 0;
890  if (!$swapIntoWS) {
891  $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
892  }
893  // Moving element.
895  // && $t3ver_state['swapVersion']==4 // Maybe we don't need this?
896  if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) {
897  $movePlhID = $plhRec['uid'];
898  $movePlh['pid'] = $swapVersion['pid'];
899  $swapVersion['pid'] = (int)$plhRec['pid'];
900  $curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state'];
901  $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
902  if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
903  // sortby is a "keepFields" which is why this will work...
904  $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
905  $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
906  }
907  }
908  }
909  // Take care of relations in each field (e.g. IRRE):
910  if (is_array($GLOBALS['TCA'][$table]['columns'])) {
911  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
912  if (isset($fieldConf['config']) && is_array($fieldConf['config'])) {
913  $this->version_swap_processFields($table, $field, $fieldConf['config'], $curVersion, $swapVersion, $tcemainObj);
914  }
915  }
916  }
917  unset($swapVersion['uid']);
918  // Modify online version to become offline:
919  unset($curVersion['uid']);
920  // Set pid for OFFLINE
921  $curVersion['pid'] = -1;
922  $curVersion['t3ver_oid'] = (int)$id;
923  $curVersion['t3ver_wsid'] = $swapIntoWS ? (int)$tmp_wsid : 0;
924  $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
925  $curVersion['t3ver_count'] = $curVersion['t3ver_count'] + 1;
926  // Increment lifecycle counter
927  $curVersion['t3ver_stage'] = 0;
928  if (!$swapIntoWS) {
929  $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
930  }
931  // Registering and swapping MM relations in current and swap records:
932  $tcemainObj->version_remapMMForVersionSwap($table, $id, $swapWith);
933  // Generating proper history data to prepare logging
934  $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
935  $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
936  // Execute swapping:
937  $sqlErrors = [];
938  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$id, $swapVersion);
939  if ($GLOBALS['TYPO3_DB']->sql_error()) {
940  $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
941  } else {
942  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$swapWith, $curVersion);
943  if ($GLOBALS['TYPO3_DB']->sql_error()) {
944  $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
945  } else {
946  unlink($lockFileName);
947  }
948  }
949  if (!empty($sqlErrors)) {
950  $tcemainObj->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
951  } else {
952  // Register swapped ids for later remapping:
953  $this->remappedIds[$table][$id] = $swapWith;
954  $this->remappedIds[$table][$swapWith] = $id;
955  // If a moving operation took place...:
956  if ($movePlhID) {
957  // Remove, if normal publishing:
958  if (!$swapIntoWS) {
959  // For delete + completely delete!
960  $tcemainObj->deleteEl($table, $movePlhID, true, true);
961  } else {
962  // Otherwise update the movePlaceholder:
963  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$movePlhID, $movePlh);
964  $tcemainObj->addRemapStackRefIndex($table, $movePlhID);
965  }
966  }
967  // Checking for delete:
968  // Delete only if new/deleted placeholders are there.
969  if (!$swapIntoWS && ((int)$t3ver_state['swapVersion'] === 1 || (int)$t3ver_state['swapVersion'] === 2)) {
970  // Force delete
971  $tcemainObj->deleteEl($table, $id, true);
972  }
973  $tcemainObj->newlog2(($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, $table, $id, $swapVersion['pid']);
974  // Update reference index of the live record:
975  $tcemainObj->addRemapStackRefIndex($table, $id);
976  // Set log entry for live record:
977  $propArr = $tcemainObj->getRecordPropertiesFromRow($table, $swapVersion);
978  if ($propArr['_ORIG_pid'] == -1) {
979  $label = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_tcemain.xlf:version_swap.offline_record_updated');
980  } else {
981  $label = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_tcemain.xlf:version_swap.online_record_updated');
982  }
983  $theLogId = $tcemainObj->log($table, $id, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
984  $tcemainObj->setHistory($table, $id, $theLogId);
985  // Update reference index of the offline record:
986  $tcemainObj->addRemapStackRefIndex($table, $swapWith);
987  // Set log entry for offline record:
988  $propArr = $tcemainObj->getRecordPropertiesFromRow($table, $curVersion);
989  if ($propArr['_ORIG_pid'] == -1) {
990  $label = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_tcemain.xlf:version_swap.offline_record_updated');
991  } else {
992  $label = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_tcemain.xlf:version_swap.online_record_updated');
993  }
994  $theLogId = $tcemainObj->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']);
995  $tcemainObj->setHistory($table, $swapWith, $theLogId);
996 
997  $stageId = -20; // \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_EXECUTE_ID;
999  $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
1000  $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
1001  $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = $table . ':' . $id;
1002  $this->notificationEmailInfo[$notificationEmailInfoKey]['alternativeRecipients'] = $notificationAlternativeRecipients;
1003  } else {
1004  $this->notifyStageChange($wsAccess, $stageId, $table, $id, $comment, $tcemainObj, $notificationAlternativeRecipients);
1005  }
1006  // Write to log with stageId -20
1007  $tcemainObj->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
1008  $tcemainObj->log($table, $id, 6, 0, 0, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
1009 
1010  // Clear cache:
1011  $tcemainObj->registerRecordIdForPageCacheClearing($table, $id);
1012  // Checking for "new-placeholder" and if found, delete it (BUT FIRST after swapping!):
1013  if (!$swapIntoWS && $t3ver_state['curVersion'] > 0) {
1014  // For delete + completely delete!
1015  $tcemainObj->deleteEl($table, $swapWith, true, true);
1016  }
1017 
1018  //Update reference index for live workspace too:
1020  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
1021  $refIndexObj->setWorkspaceId(0);
1022  $refIndexObj->updateRefIndexTable($table, $id);
1023  $refIndexObj->updateRefIndexTable($table, $swapWith);
1024  }
1025  }
1026 
1035  public function writeRemappedForeignField(\TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis, array $configuration, $parentId)
1036  {
1037  foreach ($dbAnalysis->itemArray as &$item) {
1038  if (isset($this->remappedIds[$item['table']][$item['id']])) {
1039  $item['id'] = $this->remappedIds[$item['table']][$item['id']];
1040  }
1041  }
1042  $dbAnalysis->writeForeignField($configuration, $parentId);
1043  }
1044 
1057  protected function version_swap_processFields($tableName, $fieldName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
1058  {
1059  $inlineType = $dataHandler->getInlineFieldType($configuration);
1060  if ($inlineType !== 'field') {
1061  return;
1062  }
1063  $foreignTable = $configuration['foreign_table'];
1064  // Read relations that point to the current record (e.g. live record):
1065  $liveRelations = $this->createRelationHandlerInstance();
1066  $liveRelations->setWorkspaceId(0);
1067  $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration);
1068  // Read relations that point to the record to be swapped with e.g. draft record):
1069  $versionRelations = $this->createRelationHandlerInstance();
1070  $versionRelations->setUseLiveReferenceIds(false);
1071  $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration);
1072  // Update relations for both (workspace/versioning) sites:
1073  if (count($liveRelations->itemArray)) {
1074  $dataHandler->addRemapAction(
1075  $tableName, $liveData['uid'],
1076  [$this, 'updateInlineForeignFieldSorting'],
1077  [$tableName, $liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace]
1078  );
1079  }
1080  if (count($versionRelations->itemArray)) {
1081  $dataHandler->addRemapAction(
1082  $tableName, $liveData['uid'],
1083  [$this, 'updateInlineForeignFieldSorting'],
1084  [$tableName, $liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0]
1085  );
1086  }
1087  }
1088 
1107  public function updateInlineForeignFieldSorting($parentTableName, $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
1108  {
1109  $remappedIds = [];
1110  // Use remapped ids (live id <-> version id)
1111  foreach ($foreignIds as $foreignId) {
1112  if (!empty($this->remappedIds[$foreignTableName][$foreignId])) {
1113  $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId];
1114  } else {
1115  $remappedIds[] = $foreignId;
1116  }
1117  }
1118 
1119  $relationHandler = $this->createRelationHandlerInstance();
1120  $relationHandler->setWorkspaceId($targetWorkspaceId);
1121  $relationHandler->setUseLiveReferenceIds(false);
1122  $relationHandler->start(implode(',', $remappedIds), $foreignTableName);
1123  $relationHandler->processDeletePlaceholder();
1124  $relationHandler->writeForeignField($configuration, $parentId);
1125  }
1126 
1136  protected function version_clearWSID($table, $id, $flush = false, DataHandler $tcemainObj)
1137  {
1138  if ($errorCode = $tcemainObj->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
1139  $tcemainObj->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1);
1140  return;
1141  }
1142  if (!$tcemainObj->checkRecordUpdateAccess($table, $id)) {
1143  $tcemainObj->newlog('Attempt to reset workspace for record failed because you do not have edit access', 1);
1144  return;
1145  }
1146  $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
1147  if (!$liveRec) {
1148  return;
1149  }
1150  // Clear workspace ID:
1151  $updateData = [
1152  't3ver_wsid' => 0,
1153  't3ver_tstamp' => $GLOBALS['EXEC_TIME']
1154  ];
1155  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$id, $updateData);
1156  // Clear workspace ID for live version AND DELETE IT as well because it is a new record!
1157  if (
1158  VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1159  || VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1160  ) {
1161  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$liveRec['uid'], $updateData);
1162  // THIS assumes that the record was placeholder ONLY for ONE record (namely $id)
1163  $tcemainObj->deleteEl($table, $liveRec['uid'], true);
1164  }
1165  // If "deleted" flag is set for the version that got released
1166  // it doesn't make sense to keep that "placeholder" anymore and we delete it completly.
1167  $wsRec = BackendUtility::getRecord($table, $id);
1168  if (
1169  $flush
1170  || (
1171  VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1172  || VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1173  )
1174  ) {
1175  $tcemainObj->deleteEl($table, $id, true, true);
1176  }
1177  // Remove the move-placeholder if found for live record.
1179  if ($plhRec = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid')) {
1180  $tcemainObj->deleteEl($table, $plhRec['uid'], true, true);
1181  }
1182  }
1183  }
1184 
1185  /*******************************
1186  ***** helper functions ******
1187  *******************************/
1199  protected function rawCopyPageContent($oldPageId, $newPageId, array $copyTablesArray, DataHandler $tcemainObj)
1200  {
1201  if (!$newPageId) {
1202  return;
1203  }
1204  foreach ($copyTablesArray as $table) {
1205  // all records under the page is copied.
1206  if ($table && is_array($GLOBALS['TCA'][$table]) && $table !== 'pages') {
1207  $mres = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid', $table, 'pid=' . (int)$oldPageId . $tcemainObj->deleteClause($table));
1208  while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($mres)) {
1209  // Check, if this record has already been copied by a parent record as relation:
1210  if (!$tcemainObj->copyMappingArray[$table][$row['uid']]) {
1211  // Copying each of the underlying records (method RAW)
1212  $tcemainObj->copyRecord_raw($table, $row['uid'], $newPageId);
1213  }
1214  }
1215  $GLOBALS['TYPO3_DB']->sql_free_result($mres);
1216  }
1217  }
1218  }
1219 
1228  public function findPageElementsForVersionSwap($table, $id, $offlineId)
1229  {
1230  $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid');
1231  $workspaceId = (int)$rec['t3ver_wsid'];
1232  $elementData = [];
1233  if ($workspaceId === 0) {
1234  return $elementData;
1235  }
1236  // Get page UID for LIVE and workspace
1237  if ($table != 'pages') {
1238  $rec = BackendUtility::getRecord($table, $id, 'pid');
1239  $pageId = $rec['pid'];
1240  $rec = BackendUtility::getRecord('pages', $pageId);
1241  BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1242  $offlinePageId = $rec['_ORIG_uid'];
1243  } else {
1244  $pageId = $id;
1245  $offlinePageId = $offlineId;
1246  }
1247  // Traversing all tables supporting versioning:
1248  foreach ($GLOBALS['TCA'] as $table => $cfg) {
1249  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') {
1250  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('A.uid AS offlineUid, B.uid AS uid', $table . ' A,' . $table . ' B', 'A.pid=-1 AND B.pid=' . $pageId . ' AND A.t3ver_wsid=' . $workspaceId . ' AND B.uid=A.t3ver_oid' . BackendUtility::deleteClause($table, 'A') . BackendUtility::deleteClause($table, 'B'));
1251  while (false != ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1252  $elementData[$table][] = [$row[1], $row[0]];
1253  }
1254  $GLOBALS['TYPO3_DB']->sql_free_result($res);
1255  }
1256  }
1257  if ($offlinePageId && $offlinePageId != $pageId) {
1258  $elementData['pages'][] = [$pageId, $offlinePageId];
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  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT A.uid', $table . ' A,' . $table . ' B', 'A.pid=-1' . ' AND A.t3ver_wsid=' . $workspaceId . ' AND B.pid IN (' . implode(',', $pageIdList) . ') AND A.t3ver_oid=B.uid' . BackendUtility::deleteClause($table, 'A') . BackendUtility::deleteClause($table, 'B'));
1280  while (false !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1281  $elementList[$table][] = $row[0];
1282  }
1283  $GLOBALS['TYPO3_DB']->sql_free_result($res);
1284  if (is_array($elementList[$table])) {
1285  // Yes, it is possible to get non-unique array even with DISTINCT above!
1286  // It happens because several UIDs are passed in the array already.
1287  $elementList[$table] = array_unique($elementList[$table]);
1288  }
1289  }
1290  }
1291  }
1292 
1303  public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList)
1304  {
1305  if ($workspaceId == 0) {
1306  return;
1307  }
1308  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT B.pid', $table . ' A,' . $table . ' B', 'A.pid=-1' . ' AND A.t3ver_wsid=' . $workspaceId . ' AND A.uid IN (' . implode(',', $idList) . ') AND A.t3ver_oid=B.uid' . BackendUtility::deleteClause($table, 'A') . BackendUtility::deleteClause($table, 'B'));
1309  while (false !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1310  $pageIdList[] = $row[0];
1311  // Find ws version
1312  // Note: cannot use BackendUtility::getRecordWSOL()
1313  // here because it does not accept workspace id!
1314  $rec = BackendUtility::getRecord('pages', $row[0]);
1315  BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1316  if ($rec['_ORIG_uid']) {
1317  $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1318  }
1319  }
1320  $GLOBALS['TYPO3_DB']->sql_free_result($res);
1321  // The line below is necessary even with DISTINCT
1322  // because several elements can be passed by caller
1323  $pageIdList = array_unique($pageIdList);
1324  }
1325 
1332  public function findRealPageIds(array &$idList)
1333  {
1334  foreach ($idList as $key => $id) {
1335  $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid');
1336  if ($rec['t3ver_oid'] > 0) {
1337  $idList[$key] = $rec['t3ver_oid'];
1338  }
1339  }
1340  }
1341 
1356  protected function moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $tcemainObj)
1357  {
1358  // If a record gets moved after a record that already has a placeholder record
1359  // then the new placeholder record needs to be after the existing one
1360  $originalRecordDestinationPid = $destPid;
1361  if ($destPid < 0) {
1362  $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid');
1363  if ($movePlaceHolder !== false) {
1364  $destPid = -$movePlaceHolder['uid'];
1365  }
1366  }
1367  if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) {
1368  // If already a placeholder exists, move it:
1369  $tcemainObj->moveRecord_raw($table, $plh['uid'], $destPid);
1370  } else {
1371  // First, we create a placeholder record in the Live workspace that
1372  // represents the position to where the record is eventually moved to.
1373  $newVersion_placeholderFieldArray = [];
1374 
1375  // Use property for move placeholders if set (since TYPO3 CMS 6.2)
1376  if (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'])) {
1377  $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'];
1378  // Fallback to property for new placeholder (existed long time before TYPO3 CMS 6.2)
1379  } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'])) {
1380  $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1381  }
1382 
1383  // Set values from the versioned record to the move placeholder
1384  if (!empty($shadowColumnsForMovePlaceholder)) {
1385  $versionedRecord = BackendUtility::getRecord($table, $wsUid);
1386  $shadowColumns = GeneralUtility::trimExplode(',', $shadowColumnsForMovePlaceholder, true);
1387  foreach ($shadowColumns as $shadowColumn) {
1388  if (isset($versionedRecord[$shadowColumn])) {
1389  $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn];
1390  }
1391  }
1392  }
1393 
1394  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1395  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1396  }
1397  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1398  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $tcemainObj->userid;
1399  }
1400  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
1401  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1402  }
1403  if ($table == 'pages') {
1404  // Copy page access settings from original page to placeholder
1405  $perms_clause = $tcemainObj->BE_USER->getPagePermsClause(1);
1406  $access = BackendUtility::readPageAccess($uid, $perms_clause);
1407  $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid'];
1408  $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid'];
1409  $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user'];
1410  $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group'];
1411  $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody'];
1412  }
1413  $newVersion_placeholderFieldArray['t3ver_label'] = 'MovePlaceholder #' . $uid;
1414  $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid;
1415  // Setting placeholder state value for temporary record
1416  $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
1417  // Setting workspace - only so display of place holders can filter out those from other workspaces.
1418  $newVersion_placeholderFieldArray['t3ver_wsid'] = $tcemainObj->BE_USER->workspace;
1419  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $tcemainObj->getPlaceholderTitleForTableLabel($table, 'MOVE-TO PLACEHOLDER for #' . $uid);
1420  // moving localized records requires to keep localization-settings for the placeholder too
1421  if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
1422  $l10nParentRec = BackendUtility::getRecord($table, $uid);
1423  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
1424  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
1425  if (isset($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])) {
1426  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']];
1427  }
1428  unset($l10nParentRec);
1429  }
1430  // Initially, create at root level.
1431  $newVersion_placeholderFieldArray['pid'] = 0;
1432  $id = 'NEW_MOVE_PLH';
1433  // Saving placeholder as 'original'
1434  $tcemainObj->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1435  // Move the new placeholder from temporary root-level to location:
1436  $tcemainObj->moveRecord_raw($table, $tcemainObj->substNEWwithIDs[$id], $destPid);
1437  // Move the workspace-version of the original to be the version of the move-to-placeholder:
1438  // Setting placeholder state value for version (so it can know it is currently a new version...)
1439  $updateFields = [
1440  't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER)
1441  ];
1442  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$wsUid, $updateFields);
1443  }
1444  // Check for the localizations of that element and move them as well
1445  $tcemainObj->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
1446  }
1447 
1455  protected function getPossibleInlineChildTablesOfParentTable($parentTable, array $possibleInlineChildren = [])
1456  {
1457  foreach ($GLOBALS['TCA'][$parentTable]['columns'] as $parentField => $parentFieldDefinition) {
1458  if (isset($parentFieldDefinition['config']['type'])) {
1459  $parentFieldConfiguration = $parentFieldDefinition['config'];
1460  if ($parentFieldConfiguration['type'] == 'inline' && isset($parentFieldConfiguration['foreign_table'])) {
1461  if (!in_array($parentFieldConfiguration['foreign_table'], $possibleInlineChildren)) {
1462  $possibleInlineChildren = $this->getPossibleInlineChildTablesOfParentTable($parentFieldConfiguration['foreign_table'], array_merge($possibleInlineChildren, $parentFieldConfiguration['foreign_table']));
1463  }
1464  }
1465  }
1466  }
1467  return $possibleInlineChildren;
1468  }
1469 
1476  public function getCommandMap(DataHandler $tceMain)
1477  {
1479  \TYPO3\CMS\Version\DataHandler\CommandMap::class,
1480  $this,
1481  $tceMain,
1482  $tceMain->cmdmap,
1483  $tceMain->BE_USER->workspace
1484  );
1485  }
1486 
1493  protected function getUniqueFields($table)
1494  {
1495  $listArr = [];
1496  if (empty($GLOBALS['TCA'][$table]['columns'])) {
1497  return $listArr;
1498  }
1499  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) {
1500  if ($configArr['config']['type'] === 'input') {
1501  $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], true);
1502  if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) {
1503  $listArr[] = $field;
1504  }
1505  }
1506  }
1507  return $listArr;
1508  }
1509 
1513  protected function createRelationHandlerInstance()
1514  {
1515  return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\RelationHandler::class);
1516  }
1517 }
getPlaceholderTitleForTableLabel($table, $placeholderContent=null)
static getPagesTSconfig($id, $rootLine=null, $returnPartArray=false)
int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
insertDB($table, $id, $fieldArray, $newVersion=false, $suggestedUid=0, $dontSetNewIdIndex=false)
doesBranchExist($inList, $pid, $perms, $recurse)
static readPageAccess($id, $perms_clause)
static getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields=' *')
processCmdmap_afterFinish(DataHandler $tcemainObj)
rawCopyPageContent($oldPageId, $newPageId, array $copyTablesArray, DataHandler $tcemainObj)
updateInlineForeignFieldSorting($parentTableName, $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
static isFirstPartOfStr($str, $partStr)
static forceIntegerInRange($theInt, $min, $max=2000000000, $defaultValue=0)
Definition: MathUtility.php:31
version_setStage($table, $id, $stageId, $comment='', $notificationEmailInfo=false, DataHandler $tcemainObj, array $notificationAlternativeRecipients=[])
getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix=false)
static writeFileToTypo3tempDir($filepath, $content)
static BEenableFields($table, $inv=false)
moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
processCmdmap_beforeStart(DataHandler $tcemainObj)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static workspaceOL($table, &$row, $wsid=-99, $unsetMovePointers=false)
processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $tcemainObj)
addRemapAction($table, $id, array $callback, array $arguments)
version_swap_processFields($tableName, $fieldName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
copyRecord_raw($table, $uid, $pid, $overrideArray=[], array $workspaceOptions=[])
static fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch=false)
findPageElementsForVersionSwap($table, $id, $offlineId)
versionizePages($uid, $label, $versionizeTree, DataHandler $tcemainObj)
compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
registerRecordIdForPageCacheClearing($table, $uid, $pid=null)
deleteEl($table, $uid, $noRecordCheck=false, $forceHardDelete=false)
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)
findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList)
moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $tcemainObj)
$uid
Definition: server.php:38
version_clearWSID($table, $id, $flush=false, DataHandler $tcemainObj)
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)
moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $tcemainObj)
moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration)
writeRemappedForeignField(\TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis, array $configuration, $parentId)
getPossibleInlineChildTablesOfParentTable($parentTable, array $possibleInlineChildren=[])
static deleteClause($table, $tableAlias='')
versionizeRecord($table, $id, $label, $delete=false)
moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $tcemainObj)
static arrayDiffAssocRecursive(array $array1, array $array2)