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