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