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