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