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