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