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