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