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