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