[BUGFIX] Always use system getFrom() when sending emails
[Packages/TYPO3.CMS.git] / typo3 / sysext / version / Classes / Hook / DataHandlerHook.php
1 <?php
2 namespace TYPO3\CMS\Version\Hook;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Doctrine\DBAL\DBALException;
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
19 use TYPO3\CMS\Core\Database\Connection;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
22 use TYPO3\CMS\Core\Database\ReferenceIndex;
23 use TYPO3\CMS\Core\DataHandling\DataHandler;
24 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
25 use TYPO3\CMS\Core\Utility\ArrayUtility;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Core\Versioning\VersionState;
28 use TYPO3\CMS\Lang\LanguageService;
29
30 /**
31 * Contains some parts for staging, versioning and workspaces
32 * to interact with the TYPO3 Core Engine
33 */
34 class DataHandlerHook
35 {
36 /**
37 * For accumulating information about workspace stages raised
38 * on elements so a single mail is sent as notification.
39 * previously called "accumulateForNotifEmail" in DataHandler
40 *
41 * @var array
42 */
43 protected $notificationEmailInfo = [];
44
45 /**
46 * Contains remapped IDs.
47 *
48 * @var array
49 */
50 protected $remappedIds = [];
51
52 /**
53 * @var \TYPO3\CMS\Workspaces\Service\WorkspaceService
54 */
55 protected $workspaceService;
56
57 /****************************
58 ***** Cmdmap Hooks ******
59 ****************************/
60 /**
61 * hook that is called before any cmd of the commandmap is executed
62 *
63 * @param DataHandler $dataHandler reference to the main DataHandler object
64 * @return void
65 */
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
74 /**
75 * hook that is called when no prepared command was found
76 *
77 * @param string $command the command to be executed
78 * @param string $table the table of the record
79 * @param int $id the ID of the record
80 * @param mixed $value the value containing the data
81 * @param bool $commandIsProcessed can be set so that other hooks or
82 * @param DataHandler $dataHandler reference to the main DataHandler object
83 * @return void
84 */
85 public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
86 {
87 // custom command "version"
88 if ($command === 'version') {
89 $commandIsProcessed = true;
90 $action = (string)$value['action'];
91 $comment = !empty($value['comment']) ? $value['comment'] : '';
92 $notificationAlternativeRecipients = (isset($value['notificationAlternativeRecipients'])) && is_array($value['notificationAlternativeRecipients']) ? $value['notificationAlternativeRecipients'] : [];
93 switch ($action) {
94 case 'new':
95 $dataHandler->versionizeRecord($table, $id, $value['label']);
96 break;
97 case 'swap':
98 $this->version_swap($table, $id, $value['swapWith'], $value['swapIntoWS'],
99 $dataHandler,
100 $comment,
101 true,
102 $notificationAlternativeRecipients
103 );
104 break;
105 case 'clearWSID':
106 $this->version_clearWSID($table, $id, false, $dataHandler);
107 break;
108 case 'flush':
109 $this->version_clearWSID($table, $id, true, $dataHandler);
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 $dataHandler,
118 $notificationAlternativeRecipients
119 );
120 }
121 break;
122 default:
123 // Do nothing
124 }
125 }
126 }
127
128 /**
129 * hook that is called AFTER all commands of the commandmap was
130 * executed
131 *
132 * @param DataHandler $dataHandler reference to the main DataHandler object
133 * @return void
134 */
135 public function processCmdmap_afterFinish(DataHandler $dataHandler)
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], $dataHandler, $notifItem['alternativeRecipients']);
140 }
141 // Reset notification array
142 $this->notificationEmailInfo = [];
143 // Reset remapped IDs
144 $this->remappedIds = [];
145 }
146
147 /**
148 * hook that is called when an element shall get deleted
149 *
150 * @param string $table the table of the record
151 * @param int $id the ID of the record
152 * @param array $record The accordant database record
153 * @param bool $recordWasDeleted can be set so that other hooks or
154 * @param DataHandler $dataHandler reference to the main DataHandler object
155 * @return void
156 */
157 public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
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($dataHandler->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 ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->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($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'])
185 && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
186 && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
187 && $dataHandler->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 GeneralUtility::makeInstance(ConnectionPool::class)
196 ->getConnectionForTable($table)
197 ->update(
198 $table,
199 [
200 't3ver_label' => 'DELETED!',
201 't3ver_state' => 2,
202 ],
203 ['uid' => $id]
204 );
205
206 // Delete localization overlays:
207 $dataHandler->deleteL10nOverlayRecords($table, $id);
208 } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
209 // Delete those in WS 0 + if their live records state was not "Placeholder".
210 $dataHandler->deleteEl($table, $id);
211 // Delete move-placeholder if current version record is a move-to-pointer
212 if ($recordVersionState->equals(VersionState::MOVE_POINTER)) {
213 $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']);
214 if (!empty($movePlaceholder)) {
215 $dataHandler->deleteEl($table, $movePlaceholder['uid']);
216 }
217 }
218 } else {
219 // If live record was placeholder (new/deleted), rather clear
220 // it from workspace (because it clears both version and placeholder).
221 $this->version_clearWSID($table, $id, false, $dataHandler);
222 }
223 } else {
224 $dataHandler->newlog('Tried to delete record from another workspace', 1);
225 }
226 } else {
227 $dataHandler->newlog('Versioning not enabled for record with PID = -1!', 2);
228 }
229 } elseif ($res = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($record['pid'], $table)) {
230 // Look, if record is "online" or in a versionized branch, then delete directly.
231 if ($res > 0) {
232 $dataHandler->deleteEl($table, $id);
233 } else {
234 $dataHandler->newlog('Stage of root point did not allow for deletion', 1);
235 }
236 } elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) {
237 // Placeholders for moving operations are deletable directly.
238 // Get record which its a placeholder for and reset the t3ver_state of that:
239 if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) {
240 // Clear the state flag of the workspace version of the record
241 // Setting placeholder state value for version (so it can know it is currently a new version...)
242
243 GeneralUtility::makeInstance(ConnectionPool::class)
244 ->getConnectionForTable($table)
245 ->update(
246 $table,
247 [
248 't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE)
249 ],
250 ['uid' => (int)$wsRec['uid']]
251 );
252 }
253 $dataHandler->deleteEl($table, $id);
254 } else {
255 // Otherwise, try to delete by versioning:
256 $copyMappingArray = $dataHandler->copyMappingArray;
257 $dataHandler->versionizeRecord($table, $id, 'DELETED!', true);
258 // Determine newly created versions:
259 // (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
260 $versionizedElements = ArrayUtility::arrayDiffAssocRecursive($dataHandler->copyMappingArray, $copyMappingArray);
261 // Delete localization overlays:
262 foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
263 foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
264 $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
265 }
266 }
267 }
268 }
269
270 /**
271 * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about
272 * moving records that are *not* in the live workspace
273 *
274 * @param string $table the table of the record
275 * @param int $uid the ID of the record
276 * @param int $destPid Position to move to: $destPid: >=0 then it points to
277 * @param array $propArr Record properties, like header and pid (includes workspace overlay)
278 * @param array $moveRec Record properties, like header and pid (without workspace overlay)
279 * @param int $resolvedPid The final page ID of the record
280 * @param bool $recordWasMoved can be set so that other hooks or
281 * @param DataHandler $dataHandler
282 * @return void
283 */
284 public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
285 {
286 // Only do something in Draft workspace
287 if ($dataHandler->BE_USER->workspace === 0) {
288 return;
289 }
290 if ($destPid < 0) {
291 // Fetch move placeholder, since it might point to a new page in the current workspace
292 $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid');
293 if ($movePlaceHolder !== false) {
294 $resolvedPid = $movePlaceHolder['pid'];
295 }
296 }
297 $recordWasMoved = true;
298 $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']);
299 // Get workspace version of the source record, if any:
300 $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
301 // Handle move-placeholders if the current record is not one already
302 if (
303 BackendUtility::isTableWorkspaceEnabled($table)
304 && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER)
305 ) {
306 // Create version of record first, if it does not exist
307 if (empty($WSversion['uid'])) {
308 $dataHandler->versionizeRecord($table, $uid, 'MovePointer');
309 $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
310 $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
311 // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
312 } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$WSversion['uid']) {
313 $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
314 }
315 }
316 // Check workspace permissions:
317 $workspaceAccessBlocked = [];
318 // Element was in "New/Deleted/Moved" so it can be moved...
319 $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder();
320 $destRes = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($resolvedPid, $table);
321 $canMoveRecord = ($recIsNewVersion || BackendUtility::isTableWorkspaceEnabled($table));
322 // Workspace source check:
323 if (!$recIsNewVersion) {
324 $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $WSversion['uid'] ? $WSversion['uid'] : $uid);
325 if ($errorCode) {
326 $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
327 } elseif (!$canMoveRecord && $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($moveRec['pid'], $table) <= 0) {
328 $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
329 }
330 }
331 // Workspace destination check:
332 // All records can be inserted if $destRes is greater than zero.
333 // Only new versions can be inserted if $destRes is FALSE.
334 // NO RECORDS can be inserted if $destRes is negative which indicates a stage
335 // not allowed for use. If "versioningWS" is version 2, moving can take place of versions.
336 // since TYPO3 CMS 7, version2 is the default and the only option
337 if (!($destRes > 0 || $canMoveRecord && !$destRes)) {
338 $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
339 } elseif ($destRes == 1 && $WSversion['uid']) {
340 $workspaceAccessBlocked['dest2'] = 'Could not insert other versions in destination PID ';
341 }
342 if (empty($workspaceAccessBlocked)) {
343 // If the move operation is done on a versioned record, which is
344 // NOT new/deleted placeholder and versioningWS is in version 2, then...
345 // since TYPO3 CMS 7, version2 is the default and the only option
346 if ($WSversion['uid'] && !$recIsNewVersion && BackendUtility::isTableWorkspaceEnabled($table)) {
347 $this->moveRecord_wsPlaceholders($table, $uid, $destPid, $WSversion['uid'], $dataHandler);
348 } else {
349 // moving not needed, just behave like in live workspace
350 $recordWasMoved = false;
351 }
352 } else {
353 $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), 1);
354 }
355 }
356
357 /**
358 * Processes fields of a moved record and follows references.
359 *
360 * @param DataHandler $dataHandler Calling DataHandler instance
361 * @param int $resolvedPageId Resolved real destination page id
362 * @param string $table Name of parent table
363 * @param int $uid UID of the parent record
364 * @return void
365 */
366 protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
367 {
368 $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
369 if (empty($versionedRecord)) {
370 return;
371 }
372 foreach ($versionedRecord as $field => $value) {
373 if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
374 continue;
375 }
376 $this->moveRecord_processFieldValue(
377 $dataHandler, $resolvedPageId,
378 $table, $uid, $field, $value,
379 $GLOBALS['TCA'][$table]['columns'][$field]['config']
380 );
381 }
382 }
383
384 /**
385 * Processes a single field of a moved record and follows references.
386 *
387 * @param DataHandler $dataHandler Calling DataHandler instance
388 * @param int $resolvedPageId Resolved real destination page id
389 * @param string $table Name of parent table
390 * @param int $uid UID of the parent record
391 * @param string $field Name of the field of the parent record
392 * @param string $value Value of the field of the parent record
393 * @param array $configuration TCA field configuration of the parent record
394 * @return void
395 */
396 protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration)
397 {
398 $inlineFieldType = $dataHandler->getInlineFieldType($configuration);
399 $inlineProcessing = (
400 ($inlineFieldType === 'list' || $inlineFieldType === 'field')
401 && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
402 && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
403 );
404
405 if ($inlineProcessing) {
406 if ($table === 'pages') {
407 // If the inline elements are related to a page record,
408 // make sure they reside at that page and not at its parent
409 $resolvedPageId = $uid;
410 }
411
412 $dbAnalysis = $this->createRelationHandlerInstance();
413 $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
414
415 // Moving records to a positive destination will insert each
416 // record at the beginning, thus the order is reversed here:
417 foreach ($dbAnalysis->itemArray as $item) {
418 $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
419 if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) {
420 continue;
421 }
422 $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
423 }
424 }
425 }
426
427 /****************************
428 ***** Notifications ******
429 ****************************/
430 /**
431 * Send an email notification to users in workspace
432 *
433 * @param array $stat Workspace access array from \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::checkWorkspace()
434 * @param int $stageId New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected!
435 * @param string $table Table name of element (or list of element names if $id is zero)
436 * @param int $id Record uid of element (if zero, then $table is used as reference to element(s) alone)
437 * @param string $comment User comment sent along with action
438 * @param DataHandler $dataHandler DataHandler object
439 * @param array $notificationAlternativeRecipients List of recipients to notify instead of be_users selected by sys_workspace, list is generated by workspace extension module
440 * @return void
441 */
442 protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
443 {
444 $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
445 // So, if $id is not set, then $table is taken to be the complete element name!
446 $elementName = $id ? $table . ':' . $id : $table;
447 if (!is_array($workspaceRec)) {
448 return;
449 }
450
451 // Get the new stage title from workspaces library, if workspaces extension is installed
452 if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) {
453 $stageService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\StagesService::class);
454 $newStage = $stageService->getStageTitle((int)$stageId);
455 } else {
456 // @todo CONSTANTS SHOULD BE USED - tx_service_workspace_workspaces
457 // @todo use localized labels
458 // Compile label:
459 switch ((int)$stageId) {
460 case 1:
461 $newStage = 'Ready for review';
462 break;
463 case 10:
464 $newStage = 'Ready for publishing';
465 break;
466 case -1:
467 $newStage = 'Element was rejected!';
468 break;
469 case 0:
470 $newStage = 'Rejected element was noticed and edited';
471 break;
472 default:
473 $newStage = 'Unknown state change!?';
474 }
475 }
476 if (empty($notificationAlternativeRecipients)) {
477 // Compile list of recipients:
478 $emails = [];
479 switch ((int)$stat['stagechg_notification']) {
480 case 1:
481 switch ((int)$stageId) {
482 case 1:
483 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
484 break;
485 case 10:
486 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
487 break;
488 case -1:
489 // List of elements to reject:
490 $allElements = explode(',', $elementName);
491 // Traverse them, and find the history of each
492 foreach ($allElements as $elRef) {
493 list($eTable, $eUid) = explode(':', $elRef);
494
495 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
496 ->getQueryBuilderForTable('sys_log');
497
498 $queryBuilder->getRestrictions()->removeAll();
499
500 $result = $queryBuilder
501 ->select('log_data', 'tstamp', 'userid')
502 ->from('sys_log')
503 ->where(
504 $queryBuilder->expr()->eq(
505 'action',
506 $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT)
507 ),
508 $queryBuilder->expr()->eq(
509 'details_nr',
510 $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT)
511 ),
512 $queryBuilder->expr()->eq(
513 'tablename',
514 $queryBuilder->createNamedParameter($eTable, \PDO::PARAM_STR)
515 ),
516 $queryBuilder->expr()->eq(
517 'recuid',
518 $queryBuilder->createNamedParameter($eUid, \PDO::PARAM_INT)
519 )
520 )
521 ->orderBy('uid', 'DESC')
522 ->execute();
523
524 // Find all implicated since the last stage-raise from editing to review:
525 while ($dat = $result->fetch()) {
526 $data = unserialize($dat['log_data']);
527 $emails = $this->getEmailsForStageChangeNotification($dat['userid'], true) + $emails;
528 if ($data['stage'] == 1) {
529 break;
530 }
531 }
532 }
533 break;
534 case 0:
535 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
536 break;
537 default:
538 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
539 }
540 break;
541 case 10:
542 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
543 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']) + $emails;
544 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails;
545 break;
546 default:
547 // Do nothing
548 }
549 } else {
550 $emails = $notificationAlternativeRecipients;
551 }
552 // prepare and then send the emails
553 if (!empty($emails)) {
554 // Path to record is found:
555 list($elementTable, $elementUid) = explode(':', $elementName);
556 $elementUid = (int)$elementUid;
557 $elementRecord = BackendUtility::getRecord($elementTable, $elementUid);
558 $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord);
559 if ($elementTable === 'pages') {
560 $pageUid = $elementUid;
561 } else {
562 BackendUtility::fixVersioningPid($elementTable, $elementRecord);
563 $pageUid = ($elementUid = $elementRecord['pid']);
564 }
565
566 // new way, options are
567 // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject
568 // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject
569 $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid);
570 $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.'];
571 $markers = [
572 '###RECORD_TITLE###' => $recordTitle,
573 '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20),
574 '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
575 '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir,
576 '###WORKSPACE_TITLE###' => $workspaceRec['title'],
577 '###WORKSPACE_UID###' => $workspaceRec['uid'],
578 '###ELEMENT_NAME###' => $elementName,
579 '###NEXT_STAGE###' => $newStage,
580 '###COMMENT###' => $comment,
581 // See: #30212 - keep both markers for compatibility
582 '###USER_REALNAME###' => $dataHandler->BE_USER->user['realName'],
583 '###USER_FULLNAME###' => $dataHandler->BE_USER->user['realName'],
584 '###USER_USERNAME###' => $dataHandler->BE_USER->user['username']
585 ];
586 // add marker for preview links if workspace extension is loaded
587 if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) {
588 $this->workspaceService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class);
589 // only generate the link if the marker is in the template - prevents database from getting to much entries
590 if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) {
591 $tempEmailMessage = $this->getLanguageService()->sL($emailConfig['message']);
592 } else {
593 $tempEmailMessage = $emailConfig['message'];
594 }
595 if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== false) {
596 $markers['###PREVIEW_LINK###'] = $this->workspaceService->generateWorkspacePreviewLink($elementUid);
597 }
598 unset($tempEmailMessage);
599 $markers['###SPLITTED_PREVIEW_LINK###'] = $this->workspaceService->generateWorkspaceSplittedPreviewLink($elementUid, true);
600 }
601 // Hook for preprocessing of the content for formmails:
602 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'])) {
603 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] as $_classRef) {
604 $_procObj =& GeneralUtility::getUserObj($_classRef);
605 $markers = $_procObj->postModifyMarkers($markers, $this);
606 }
607 }
608 // send an email to each individual user, to ensure the
609 // multilanguage version of the email
610 $emailRecipients = [];
611 // an array of language objects that are needed
612 // for emails with different languages
613 $languageObjects = [
614 $this->getLanguageService()->lang => $this->getLanguageService()
615 ];
616 // loop through each recipient and send the email
617 foreach ($emails as $recipientData) {
618 // don't send an email twice
619 if (isset($emailRecipients[$recipientData['email']])) {
620 continue;
621 }
622 $emailSubject = $emailConfig['subject'];
623 $emailMessage = $emailConfig['message'];
624 $emailRecipients[$recipientData['email']] = $recipientData['email'];
625 // check if the email needs to be localized
626 // in the users' language
627 if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
628 $recipientLanguage = $recipientData['lang'] ? $recipientData['lang'] : 'default';
629 if (!isset($languageObjects[$recipientLanguage])) {
630 // a LANG object in this language hasn't been
631 // instantiated yet, so this is done here
632 /** @var $languageObject \TYPO3\CMS\Lang\LanguageService */
633 $languageObject = GeneralUtility::makeInstance(\TYPO3\CMS\Lang\LanguageService::class);
634 $languageObject->init($recipientLanguage);
635 $languageObjects[$recipientLanguage] = $languageObject;
636 } else {
637 $languageObject = $languageObjects[$recipientLanguage];
638 }
639 if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) {
640 $emailSubject = $languageObject->sL($emailSubject);
641 }
642 if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
643 $emailMessage = $languageObject->sL($emailMessage);
644 }
645 }
646 $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
647 $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true);
648 $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true);
649 // Send an email to the recipient
650 /** @var $mail \TYPO3\CMS\Core\Mail\MailMessage */
651 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
652 if (!empty($recipientData['realName'])) {
653 $recipient = [$recipientData['email'] => $recipientData['realName']];
654 } else {
655 $recipient = $recipientData['email'];
656 }
657 $mail->setTo($recipient)
658 ->setSubject($emailSubject)
659 ->setBody($emailMessage);
660 $mail->send();
661 }
662 $emailRecipients = implode(',', $emailRecipients);
663 $dataHandler->newlog2('Notification email for stage change was sent to "' . $emailRecipients . '"', $table, $id);
664 }
665 }
666
667 /**
668 * Return be_users that should be notified on stage change from input list.
669 * previously called notifyStageChange_getEmails() in DataHandler
670 *
671 * @param string $listOfUsers List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set.
672 * @param bool $noTablePrefix If set, the input list are integers and not strings.
673 * @return array Array of emails
674 */
675 protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = false)
676 {
677 $users = GeneralUtility::trimExplode(',', $listOfUsers, true);
678 $emails = [];
679 foreach ($users as $userIdent) {
680 if ($noTablePrefix) {
681 $id = (int)$userIdent;
682 } else {
683 list($table, $id) = GeneralUtility::revExplode('_', $userIdent, 2);
684 }
685 if ($table === 'be_users' || $noTablePrefix) {
686 if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) {
687 if (trim($userRecord['email']) !== '') {
688 $emails[$id] = $userRecord;
689 }
690 }
691 }
692 }
693 return $emails;
694 }
695
696 /****************************
697 ***** Stage Changes ******
698 ****************************/
699 /**
700 * Setting stage of record
701 *
702 * @param string $table Table name
703 * @param int $integer Record UID
704 * @param int $stageId Stage ID to set
705 * @param string $comment Comment that goes into log
706 * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email?
707 * @param DataHandler $dataHandler DataHandler object
708 * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
709 * @return void
710 */
711 protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = false, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
712 {
713 if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
714 $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, 1);
715 } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) {
716 $record = BackendUtility::getRecord($table, $id);
717 $stat = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']);
718 // check if the usere is allowed to the current stage, so it's also allowed to send to next stage
719 if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
720 // Set stage of record:
721 GeneralUtility::makeInstance(ConnectionPool::class)
722 ->getConnectionForTable($table)
723 ->update(
724 $table,
725 [
726 't3ver_stage' => $stageId,
727 ],
728 ['uid' => (int)$id]
729 );
730 $dataHandler->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
731 // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
732 $dataHandler->log($table, $id, 6, 0, 0, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]);
733 if ((int)$stat['stagechg_notification'] > 0) {
734 if ($notificationEmailInfo) {
735 $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$stat, $stageId, $comment];
736 $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id;
737 $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients;
738 } else {
739 $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients);
740 }
741 }
742 } else {
743 $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1);
744 }
745 } else {
746 $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', 1);
747 }
748 }
749
750 /*****************************
751 ***** CMD versioning ******
752 *****************************/
753
754 /**
755 * Swapping versions of a record
756 * Version from archive (future/past, called "swap version") will get the uid of the "t3ver_oid", the official element with uid = "t3ver_oid" will get the new versions old uid. PIDs are swapped also
757 *
758 * @param string $table Table name
759 * @param int $id UID of the online record to swap
760 * @param int $swapWith UID of the archived version to swap with!
761 * @param bool $swapIntoWS If set, swaps online into workspace instead of publishing out of workspace.
762 * @param DataHandler $dataHandler DataHandler object
763 * @param string $comment Notification comment
764 * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email?
765 * @param array $notificationAlternativeRecipients comma separated list of recipients to notificate instead of normal be_users
766 * @return void
767 */
768 protected function version_swap($table, $id, $swapWith, $swapIntoWS = 0, DataHandler $dataHandler, $comment = '', $notificationEmailInfo = false, $notificationAlternativeRecipients = [])
769 {
770
771 // Check prerequisites before start swapping
772
773 // Skip records that have been deleted during the current execution
774 if ($dataHandler->hasDeletedRecord($table, $id)) {
775 return;
776 }
777
778 // First, check if we may actually edit the online record
779 if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
780 $dataHandler->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1);
781 return;
782 }
783 // Select the two versions:
784 $curVersion = BackendUtility::getRecord($table, $id, '*');
785 $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
786 $movePlh = [];
787 $movePlhID = 0;
788 if (!(is_array($curVersion) && is_array($swapVersion))) {
789 $dataHandler->newlog('Error: Either online or swap version could not be selected!', 2);
790 return;
791 }
792 if (!$dataHandler->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
793 $dataHandler->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1);
794 return;
795 }
796 $wsAccess = $dataHandler->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']);
797 if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10)) {
798 $dataHandler->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1);
799 return;
800 }
801 if (!($dataHandler->doesRecordExist($table, $swapWith, 'show') && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) {
802 $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', 1);
803 return;
804 }
805 if ($swapIntoWS && !$dataHandler->BE_USER->workspaceSwapAccess()) {
806 $dataHandler->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1);
807 return;
808 }
809 // Check if the swapWith record really IS a version of the original!
810 if (!(((int)$swapVersion['pid'] == -1 && (int)$curVersion['pid'] >= 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
811 $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);
812 return;
813 }
814 // Lock file name:
815 $lockFileName = PATH_site . 'typo3temp/var/swap_locking/' . $table . '_' . $id . '.ser';
816 if (@is_file($lockFileName)) {
817 $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);
818 return;
819 }
820
821 // Now start to swap records by first creating the lock file
822
823 // Write lock-file:
824 GeneralUtility::writeFileToTypo3tempDir($lockFileName, serialize([
825 'tstamp' => $GLOBALS['EXEC_TIME'],
826 'user' => $dataHandler->BE_USER->user['username'],
827 'curVersion' => $curVersion,
828 'swapVersion' => $swapVersion
829 ]));
830 // Find fields to keep
831 $keepFields = $this->getUniqueFields($table);
832 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
833 $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
834 }
835 // l10n-fields must be kept otherwise the localization
836 // will be lost during the publishing
837 if ($table !== 'pages_language_overlay' && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
838 $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
839 }
840 // Swap "keepfields"
841 foreach ($keepFields as $fN) {
842 $tmp = $swapVersion[$fN];
843 $swapVersion[$fN] = $curVersion[$fN];
844 $curVersion[$fN] = $tmp;
845 }
846 // Preserve states:
847 $t3ver_state = [];
848 $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
849 $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
850 // Modify offline version to become online:
851 $tmp_wsid = $swapVersion['t3ver_wsid'];
852 // Set pid for ONLINE
853 $swapVersion['pid'] = (int)$curVersion['pid'];
854 // We clear this because t3ver_oid only make sense for offline versions
855 // and we want to prevent unintentional misuse of this
856 // value for online records.
857 $swapVersion['t3ver_oid'] = 0;
858 // In case of swapping and the offline record has a state
859 // (like 2 or 4 for deleting or move-pointer) we set the
860 // current workspace ID so the record is not deselected
861 // in the interface by BackendUtility::versioningPlaceholderClause()
862 $swapVersion['t3ver_wsid'] = 0;
863 if ($swapIntoWS) {
864 if ($t3ver_state['swapVersion'] > 0) {
865 $swapVersion['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
866 } else {
867 $swapVersion['t3ver_wsid'] = (int)$curVersion['t3ver_wsid'];
868 }
869 }
870 $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
871 $swapVersion['t3ver_stage'] = 0;
872 if (!$swapIntoWS) {
873 $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
874 }
875 // Moving element.
876 if (BackendUtility::isTableWorkspaceEnabled($table)) {
877 // && $t3ver_state['swapVersion']==4 // Maybe we don't need this?
878 if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) {
879 $movePlhID = $plhRec['uid'];
880 $movePlh['pid'] = $swapVersion['pid'];
881 $swapVersion['pid'] = (int)$plhRec['pid'];
882 $curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state'];
883 $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
884 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
885 // sortby is a "keepFields" which is why this will work...
886 $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
887 $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
888 }
889 }
890 }
891 // Take care of relations in each field (e.g. IRRE):
892 if (is_array($GLOBALS['TCA'][$table]['columns'])) {
893 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
894 $this->version_swap_processFields($table, $field, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler);
895 }
896 }
897 unset($swapVersion['uid']);
898 // Modify online version to become offline:
899 unset($curVersion['uid']);
900 // Set pid for OFFLINE
901 $curVersion['pid'] = -1;
902 $curVersion['t3ver_oid'] = (int)$id;
903 $curVersion['t3ver_wsid'] = $swapIntoWS ? (int)$tmp_wsid : 0;
904 $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
905 $curVersion['t3ver_count'] = $curVersion['t3ver_count'] + 1;
906 // Increment lifecycle counter
907 $curVersion['t3ver_stage'] = 0;
908 if (!$swapIntoWS) {
909 $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
910 }
911 // Registering and swapping MM relations in current and swap records:
912 $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith);
913 // Generating proper history data to prepare logging
914 $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
915 $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
916
917 // Execute swapping:
918 $sqlErrors = [];
919 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
920 try {
921 $connection->update(
922 $table,
923 $swapVersion,
924 ['uid' => (int)$id]
925 );
926 } catch (DBALException $e) {
927 $sqlErrors[] = $e->getPrevious()->getMessage();
928 }
929
930 if (empty($sqlErrors)) {
931 try {
932 $connection->update(
933 $table,
934 $curVersion,
935 ['uid' => (int)$swapWith]
936 );
937 unlink($lockFileName);
938 } catch (DBALException $e) {
939 $sqlErrors[] = $e->getPrevious()->getMessage();
940 }
941 }
942
943 if (!empty($sqlErrors)) {
944 $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
945 } else {
946 // Register swapped ids for later remapping:
947 $this->remappedIds[$table][$id] = $swapWith;
948 $this->remappedIds[$table][$swapWith] = $id;
949 // If a moving operation took place...:
950 if ($movePlhID) {
951 // Remove, if normal publishing:
952 if (!$swapIntoWS) {
953 // For delete + completely delete!
954 $dataHandler->deleteEl($table, $movePlhID, true, true);
955 } else {
956 // Otherwise update the movePlaceholder:
957 GeneralUtility::makeInstance(ConnectionPool::class)
958 ->getConnectionForTable($table)
959 ->update(
960 $table,
961 $movePlh,
962 ['uid' => (int)$movePlhID]
963 );
964 $dataHandler->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 $dataHandler->deleteEl($table, $id, true);
972 }
973 $dataHandler->newlog2(($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, $table, $id, $swapVersion['pid']);
974 // Update reference index of the live record:
975 $dataHandler->addRemapStackRefIndex($table, $id);
976 // Set log entry for live record:
977 $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion);
978 if ($propArr['_ORIG_pid'] == -1) {
979 $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
980 } else {
981 $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
982 }
983 $theLogId = $dataHandler->log($table, $id, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
984 $dataHandler->setHistory($table, $id, $theLogId);
985 // Update reference index of the offline record:
986 $dataHandler->addRemapStackRefIndex($table, $swapWith);
987 // Set log entry for offline record:
988 $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion);
989 if ($propArr['_ORIG_pid'] == -1) {
990 $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
991 } else {
992 $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
993 }
994 $theLogId = $dataHandler->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']);
995 $dataHandler->setHistory($table, $swapWith, $theLogId);
996
997 $stageId = -20; // \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_EXECUTE_ID;
998 if ($notificationEmailInfo) {
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, $dataHandler, $notificationAlternativeRecipients);
1005 }
1006 // Write to log with stageId -20
1007 $dataHandler->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
1008 $dataHandler->log($table, $id, 6, 0, 0, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
1009
1010 // Clear cache:
1011 $dataHandler->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 $dataHandler->deleteEl($table, $swapWith, true, true);
1016 }
1017
1018 //Update reference index for live workspace too:
1019 /** @var $refIndexObj \TYPO3\CMS\Core\Database\ReferenceIndex */
1020 $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
1021 $refIndexObj->setWorkspaceId(0);
1022 $refIndexObj->updateRefIndexTable($table, $id);
1023 $refIndexObj->updateRefIndexTable($table, $swapWith);
1024 }
1025 }
1026
1027 /**
1028 * Writes remapped foreign field (IRRE).
1029 *
1030 * @param \TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis Instance that holds the sorting order of child records
1031 * @param array $configuration The TCA field configuration
1032 * @param int $parentId The uid of the parent record
1033 * @return void
1034 */
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
1045 /**
1046 * Processes fields of a record for the publishing/swapping process.
1047 * Basically this takes care of IRRE (type "inline") child references.
1048 *
1049 * @param string $tableName Table name
1050 * @param string $fieldName: Field name
1051 * @param array $configuration TCA field configuration
1052 * @param array $liveData: Live record data
1053 * @param array $versionData: Version record data
1054 * @param DataHandler $dataHandler Calling data-handler object
1055 * @return void
1056 */
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
1089 /**
1090 * Updates foreign field sorting values of versioned and live
1091 * parents after(!) the whole structure has been published.
1092 *
1093 * This method is used as callback function in
1094 * DataHandlerHook::version_swap_procBasedOnFieldType().
1095 * Sorting fields ("sortby") are not modified during the
1096 * workspace publishing/swapping process directly.
1097 *
1098 * @param string $parentTableName
1099 * @param string $parentId
1100 * @param string $foreignTableName
1101 * @param int[] $foreignIds
1102 * @param array $configuration
1103 * @param int $targetWorkspaceId
1104 * @return void
1105 * @internal
1106 */
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
1127 /**
1128 * Release version from this workspace (and into "Live" workspace but as an offline version).
1129 *
1130 * @param string $table Table name
1131 * @param int $id Record UID
1132 * @param bool $flush If set, will completely delete element
1133 * @param DataHandler $dataHandler DataHandler object
1134 * @return void
1135 */
1136 protected function version_clearWSID($table, $id, $flush = false, DataHandler $dataHandler)
1137 {
1138 if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
1139 $dataHandler->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1);
1140 return;
1141 }
1142 if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
1143 $dataHandler->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 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1156 $connection->update(
1157 $table,
1158 $updateData,
1159 ['uid' => (int)$id]
1160 );
1161
1162 // Clear workspace ID for live version AND DELETE IT as well because it is a new record!
1163 if (
1164 VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1165 || VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1166 ) {
1167 $connection->update(
1168 $table,
1169 $updateData,
1170 ['uid' => (int)$liveRec['uid']]
1171 );
1172
1173 // THIS assumes that the record was placeholder ONLY for ONE record (namely $id)
1174 $dataHandler->deleteEl($table, $liveRec['uid'], true);
1175 }
1176 // If "deleted" flag is set for the version that got released
1177 // it doesn't make sense to keep that "placeholder" anymore and we delete it completly.
1178 $wsRec = BackendUtility::getRecord($table, $id);
1179 if (
1180 $flush
1181 || (
1182 VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1183 || VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1184 )
1185 ) {
1186 $dataHandler->deleteEl($table, $id, true, true);
1187 }
1188 // Remove the move-placeholder if found for live record.
1189 if (BackendUtility::isTableWorkspaceEnabled($table)) {
1190 if ($plhRec = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid')) {
1191 $dataHandler->deleteEl($table, $plhRec['uid'], true, true);
1192 }
1193 }
1194 }
1195
1196 /*******************************
1197 ***** helper functions ******
1198 *******************************/
1199
1200 /**
1201 * Finds all elements for swapping versions in workspace
1202 *
1203 * @param string $table Table name of the original element to swap
1204 * @param int $id UID of the original element to swap (online)
1205 * @param int $offlineId As above but offline
1206 * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID
1207 */
1208 public function findPageElementsForVersionSwap($table, $id, $offlineId)
1209 {
1210 $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid');
1211 $workspaceId = (int)$rec['t3ver_wsid'];
1212 $elementData = [];
1213 if ($workspaceId === 0) {
1214 return $elementData;
1215 }
1216 // Get page UID for LIVE and workspace
1217 if ($table !== 'pages') {
1218 $rec = BackendUtility::getRecord($table, $id, 'pid');
1219 $pageId = $rec['pid'];
1220 $rec = BackendUtility::getRecord('pages', $pageId);
1221 BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1222 $offlinePageId = $rec['_ORIG_uid'];
1223 } else {
1224 $pageId = $id;
1225 $offlinePageId = $offlineId;
1226 }
1227 // Traversing all tables supporting versioning:
1228 foreach ($GLOBALS['TCA'] as $table => $cfg) {
1229 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') {
1230 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1231 ->getQueryBuilderForTable($table);
1232
1233 $queryBuilder->getRestrictions()
1234 ->removeAll()
1235 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1236
1237 $statement = $queryBuilder
1238 ->select('A.uid AS offlineUid', 'B.uid AS uid')
1239 ->from($table, 'A')
1240 ->from($table, 'B')
1241 ->where(
1242 $queryBuilder->expr()->eq(
1243 'A.pid',
1244 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1245 ),
1246 $queryBuilder->expr()->eq(
1247 'B.pid',
1248 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1249 ),
1250 $queryBuilder->expr()->eq(
1251 'A.t3ver_wsid',
1252 $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1253 ),
1254 $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1255 )
1256 ->execute();
1257
1258 while ($row = $statement->fetch()) {
1259 $elementData[$table][] = [$row['uid'], $row['offlineUid']];
1260 }
1261 }
1262 }
1263 if ($offlinePageId && $offlinePageId != $pageId) {
1264 $elementData['pages'][] = [$pageId, $offlinePageId];
1265 }
1266
1267 return $elementData;
1268 }
1269
1270 /**
1271 * Searches for all elements from all tables on the given pages in the same workspace.
1272 *
1273 * @param array $pageIdList List of PIDs to search
1274 * @param int $workspaceId Workspace ID
1275 * @param array $elementList List of found elements. Key is table name, value is array of element UIDs
1276 * @return void
1277 */
1278 public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList)
1279 {
1280 if ($workspaceId == 0) {
1281 return;
1282 }
1283 // Traversing all tables supporting versioning:
1284 foreach ($GLOBALS['TCA'] as $table => $cfg) {
1285 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') {
1286 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1287 ->getQueryBuilderForTable($table);
1288
1289 $queryBuilder->getRestrictions()
1290 ->removeAll()
1291 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1292
1293 $statement = $queryBuilder
1294 ->select('A.uid')
1295 ->from($table, 'A')
1296 ->from($table, 'B')
1297 ->where(
1298 $queryBuilder->expr()->eq(
1299 'A.pid',
1300 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1301 ),
1302 $queryBuilder->expr()->in(
1303 'B.pid',
1304 $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY)
1305 ),
1306 $queryBuilder->expr()->eq(
1307 'A.t3ver_wsid',
1308 $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1309 ),
1310 $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1311 )
1312 ->groupBy('A.uid')
1313 ->execute();
1314
1315 while ($row = $statement->fetch()) {
1316 $elementList[$table][] = $row['uid'];
1317 }
1318 if (is_array($elementList[$table])) {
1319 // Yes, it is possible to get non-unique array even with DISTINCT above!
1320 // It happens because several UIDs are passed in the array already.
1321 $elementList[$table] = array_unique($elementList[$table]);
1322 }
1323 }
1324 }
1325 }
1326
1327 /**
1328 * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1329 *
1330 * @param string $table Table to search
1331 * @param array $idList List of records' UIDs
1332 * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publisg DRAFT from ws module!
1333 * @param array $pageIdList List of found page UIDs
1334 * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs
1335 * @return void
1336 */
1337 public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList)
1338 {
1339 if ($workspaceId == 0) {
1340 return;
1341 }
1342
1343 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1344 ->getQueryBuilderForTable($table);
1345 $queryBuilder->getRestrictions()
1346 ->removeAll()
1347 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1348
1349 $statement = $queryBuilder
1350 ->select('B.pid')
1351 ->from($table, 'A')
1352 ->from($table, 'B')
1353 ->where(
1354 $queryBuilder->expr()->eq(
1355 'A.pid',
1356 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1357 ),
1358 $queryBuilder->expr()->eq(
1359 'A.t3ver_wsid',
1360 $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1361 ),
1362 $queryBuilder->expr()->in(
1363 'A.uid',
1364 $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
1365 ),
1366 $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1367 )
1368 ->groupBy('B.pid')
1369 ->execute();
1370
1371 while ($row = $statement->fetch()) {
1372 $pageIdList[] = $row['pid'];
1373 // Find ws version
1374 // Note: cannot use BackendUtility::getRecordWSOL()
1375 // here because it does not accept workspace id!
1376 $rec = BackendUtility::getRecord('pages', $row[0]);
1377 BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1378 if ($rec['_ORIG_uid']) {
1379 $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1380 }
1381 }
1382 // The line below is necessary even with DISTINCT
1383 // because several elements can be passed by caller
1384 $pageIdList = array_unique($pageIdList);
1385 }
1386
1387 /**
1388 * Finds real page IDs for state change.
1389 *
1390 * @param array $idList List of page UIDs, possibly versioned
1391 * @return void
1392 */
1393 public function findRealPageIds(array &$idList)
1394 {
1395 foreach ($idList as $key => $id) {
1396 $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid');
1397 if ($rec['t3ver_oid'] > 0) {
1398 $idList[$key] = $rec['t3ver_oid'];
1399 }
1400 }
1401 }
1402
1403 /**
1404 * Creates a move placeholder for workspaces.
1405 * USE ONLY INTERNALLY
1406 * Moving placeholder: Can be done because the system sees it as a placeholder for NEW elements like t3ver_state=VersionState::NEW_PLACEHOLDER
1407 * Moving original: Will either create the placeholder if it doesn't exist or move existing placeholder in workspace.
1408 *
1409 * @param string $table Table name to move
1410 * @param int $uid Record uid to move (online record)
1411 * @param int $destPid Position to move to: $destPid: >=0 then it points to a page-id on which to insert the record (as the first element). <0 then it points to a uid from its own table after which to insert it (works if
1412 * @param int $wsUid UID of offline version of online record
1413 * @param DataHandler $dataHandler DataHandler object
1414 * @return void
1415 * @see moveRecord()
1416 */
1417 protected function moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $dataHandler)
1418 {
1419 // If a record gets moved after a record that already has a placeholder record
1420 // then the new placeholder record needs to be after the existing one
1421 $originalRecordDestinationPid = $destPid;
1422 if ($destPid < 0) {
1423 $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid');
1424 if ($movePlaceHolder !== false) {
1425 $destPid = -$movePlaceHolder['uid'];
1426 }
1427 }
1428 if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) {
1429 // If already a placeholder exists, move it:
1430 $dataHandler->moveRecord_raw($table, $plh['uid'], $destPid);
1431 } else {
1432 // First, we create a placeholder record in the Live workspace that
1433 // represents the position to where the record is eventually moved to.
1434 $newVersion_placeholderFieldArray = [];
1435
1436 // Use property for move placeholders if set (since TYPO3 CMS 6.2)
1437 if (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'])) {
1438 $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'];
1439 // Fallback to property for new placeholder (existed long time before TYPO3 CMS 6.2)
1440 } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'])) {
1441 $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1442 }
1443
1444 // Set values from the versioned record to the move placeholder
1445 if (!empty($shadowColumnsForMovePlaceholder)) {
1446 $versionedRecord = BackendUtility::getRecord($table, $wsUid);
1447 $shadowColumns = GeneralUtility::trimExplode(',', $shadowColumnsForMovePlaceholder, true);
1448 foreach ($shadowColumns as $shadowColumn) {
1449 if (isset($versionedRecord[$shadowColumn])) {
1450 $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn];
1451 }
1452 }
1453 }
1454
1455 if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1456 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1457 }
1458 if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1459 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $dataHandler->userid;
1460 }
1461 if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
1462 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1463 }
1464 if ($table === 'pages') {
1465 // Copy page access settings from original page to placeholder
1466 $perms_clause = $dataHandler->BE_USER->getPagePermsClause(1);
1467 $access = BackendUtility::readPageAccess($uid, $perms_clause);
1468 $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid'];
1469 $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid'];
1470 $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user'];
1471 $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group'];
1472 $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody'];
1473 }
1474 $newVersion_placeholderFieldArray['t3ver_label'] = 'MovePlaceholder #' . $uid;
1475 $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid;
1476 // Setting placeholder state value for temporary record
1477 $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
1478 // Setting workspace - only so display of place holders can filter out those from other workspaces.
1479 $newVersion_placeholderFieldArray['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
1480 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $dataHandler->getPlaceholderTitleForTableLabel($table, 'MOVE-TO PLACEHOLDER for #' . $uid);
1481 // moving localized records requires to keep localization-settings for the placeholder too
1482 if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
1483 $l10nParentRec = BackendUtility::getRecord($table, $uid);
1484 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
1485 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
1486 if (isset($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])) {
1487 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']];
1488 }
1489 unset($l10nParentRec);
1490 }
1491 // Initially, create at root level.
1492 $newVersion_placeholderFieldArray['pid'] = 0;
1493 $id = 'NEW_MOVE_PLH';
1494 // Saving placeholder as 'original'
1495 $dataHandler->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1496 // Move the new placeholder from temporary root-level to location:
1497 $dataHandler->moveRecord_raw($table, $dataHandler->substNEWwithIDs[$id], $destPid);
1498 // Move the workspace-version of the original to be the version of the move-to-placeholder:
1499 // Setting placeholder state value for version (so it can know it is currently a new version...)
1500 $updateFields = [
1501 't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER)
1502 ];
1503
1504 GeneralUtility::makeInstance(ConnectionPool::class)
1505 ->getConnectionForTable($table)
1506 ->update(
1507 $table,
1508 $updateFields,
1509 ['uid' => (int)$wsUid]
1510 );
1511 }
1512 // Check for the localizations of that element and move them as well
1513 $dataHandler->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
1514 }
1515
1516 /**
1517 * Gets an instance of the command map helper.
1518 *
1519 * @param DataHandler $dataHandler DataHandler object
1520 * @return \TYPO3\CMS\Version\DataHandler\CommandMap
1521 */
1522 public function getCommandMap(DataHandler $dataHandler)
1523 {
1524 return GeneralUtility::makeInstance(
1525 \TYPO3\CMS\Version\DataHandler\CommandMap::class,
1526 $this,
1527 $dataHandler,
1528 $dataHandler->cmdmap,
1529 $dataHandler->BE_USER->workspace
1530 );
1531 }
1532
1533 /**
1534 * Returns all fieldnames from a table which have the unique evaluation type set.
1535 *
1536 * @param string $table Table name
1537 * @return array Array of fieldnames
1538 */
1539 protected function getUniqueFields($table)
1540 {
1541 $listArr = [];
1542 if (empty($GLOBALS['TCA'][$table]['columns'])) {
1543 return $listArr;
1544 }
1545 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) {
1546 if ($configArr['config']['type'] === 'input') {
1547 $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], true);
1548 if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) {
1549 $listArr[] = $field;
1550 }
1551 }
1552 }
1553 return $listArr;
1554 }
1555
1556 /**
1557 * @return \TYPO3\CMS\Core\Database\RelationHandler
1558 */
1559 protected function createRelationHandlerInstance()
1560 {
1561 return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\RelationHandler::class);
1562 }
1563
1564 /**
1565 * @return LanguageService
1566 */
1567 protected function getLanguageService()
1568 {
1569 return $GLOBALS['LANG'];
1570 }
1571 }