/
DataHandlerHook.php
1509 lines (1425 loc) · 73.8 KB
/
DataHandlerHook.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Workspaces\Hook;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\WorkspaceAspect;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\DataHandling\PlaceholderShadowColumnsResolver;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
use TYPO3\CMS\Core\SysLog\Action\Database as DatabaseAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
use TYPO3\CMS\Workspaces\DataHandler\CommandMap;
use TYPO3\CMS\Workspaces\Notification\StageChangeNotification;
use TYPO3\CMS\Workspaces\Service\StagesService;
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
/**
* Contains some parts for staging, versioning and workspaces
* to interact with the TYPO3 Core Engine
* @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
*/
class DataHandlerHook
{
/**
* For accumulating information about workspace stages raised
* on elements so a single mail is sent as notification.
*
* @var array
*/
protected $notificationEmailInfo = [];
/**
* Contains remapped IDs.
*
* @var array
*/
protected $remappedIds = [];
/****************************
***** Cmdmap Hooks ******
****************************/
/**
* hook that is called before any cmd of the commandmap is executed
*
* @param DataHandler $dataHandler reference to the main DataHandler object
*/
public function processCmdmap_beforeStart(DataHandler $dataHandler)
{
// Reset notification array
$this->notificationEmailInfo = [];
// Resolve dependencies of version/workspaces actions:
$dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get();
}
/**
* hook that is called when no prepared command was found
*
* @param string $command the command to be executed
* @param string $table the table of the record
* @param int $id the ID of the record
* @param mixed $value the value containing the data
* @param bool $commandIsProcessed can be set so that other hooks or
* @param DataHandler $dataHandler reference to the main DataHandler object
*/
public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
{
// custom command "version"
if ($command !== 'version') {
return;
}
$commandIsProcessed = true;
$action = (string)$value['action'];
$comment = $value['comment'] ?: '';
$notificationAlternativeRecipients = $value['notificationAlternativeRecipients'] ?? [];
switch ($action) {
case 'new':
$dataHandler->versionizeRecord($table, $id, $value['label']);
break;
case 'swap':
case 'publish':
$this->version_swap(
$table,
$id,
$value['swapWith'],
$dataHandler,
$comment,
$notificationAlternativeRecipients
);
break;
case 'clearWSID':
$this->version_clearWSID($table, (int)$id, false, $dataHandler);
break;
case 'flush':
$this->version_clearWSID($table, (int)$id, true, $dataHandler);
break;
case 'setStage':
$elementIds = GeneralUtility::intExplode(',', (string)$id, true);
foreach ($elementIds as $elementId) {
$this->version_setStage(
$table,
$elementId,
$value['stageId'],
$comment,
$dataHandler,
$notificationAlternativeRecipients
);
}
break;
default:
// Do nothing
}
}
/**
* hook that is called AFTER all commands of the commandmap was
* executed
*
* @param DataHandler $dataHandler reference to the main DataHandler object
*/
public function processCmdmap_afterFinish(DataHandler $dataHandler)
{
// Empty accumulation array
$emailNotificationService = GeneralUtility::makeInstance(StageChangeNotification::class);
$this->sendStageChangeNotification(
$this->notificationEmailInfo,
$emailNotificationService,
$dataHandler
);
// Reset notification array
$this->notificationEmailInfo = [];
// Reset remapped IDs
$this->remappedIds = [];
$this->flushWorkspaceCacheEntriesByWorkspaceId((int)$dataHandler->BE_USER->workspace);
}
protected function sendStageChangeNotification(
array $accumulatedNotificationInformation,
StageChangeNotification $notificationService,
DataHandler $dataHandler
): void {
foreach ($accumulatedNotificationInformation as $groupedNotificationInformation) {
$emails = (array)$groupedNotificationInformation['recipients'];
if (empty($emails)) {
continue;
}
$workspaceRec = $groupedNotificationInformation['shared'][0];
if (!is_array($workspaceRec)) {
continue;
}
$notificationService->notifyStageChange(
$workspaceRec,
(int)$groupedNotificationInformation['shared'][1],
$groupedNotificationInformation['elements'],
$groupedNotificationInformation['shared'][2],
$emails,
$dataHandler->BE_USER
);
if ($dataHandler->enableLogging) {
[$elementTable, $elementUid] = reset($groupedNotificationInformation['elements']);
$propertyArray = $dataHandler->getRecordProperties($elementTable, $elementUid);
$pid = $propertyArray['pid'];
$dataHandler->log($elementTable, $elementUid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Notification email for stage change was sent to "' . implode('", "', $emails) . '"', -1, [], $dataHandler->eventPid($elementTable, $elementUid, $pid));
}
}
}
/**
* hook that is called when an element shall get deleted
*
* @param string $table the table of the record
* @param int $id the ID of the record
* @param array $record The accordant database record
* @param bool $recordWasDeleted can be set so that other hooks or
* @param DataHandler $dataHandler reference to the main DataHandler object
*/
public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
{
// only process the hook if it wasn't processed
// by someone else before
if ($recordWasDeleted) {
return;
}
$recordWasDeleted = true;
// For Live version, try if there is a workspace version because if so, rather "delete" that instead
// Look, if record is an offline version, then delete directly:
if ((int)($record['t3ver_oid'] ?? 0) === 0) {
if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) {
$record = $wsVersion;
$id = $record['uid'];
}
}
$recordVersionState = VersionState::cast($record['t3ver_state']);
// Look, if record is an offline version, then delete directly:
if ((int)($record['t3ver_oid'] ?? 0) > 0) {
if (BackendUtility::isTableWorkspaceEnabled($table)) {
// In Live workspace, delete any. In other workspaces there must be match.
if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) {
$liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
// Processing can be skipped if a delete placeholder shall be published
// during the current request. Thus it will be deleted later on...
$liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']);
if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid'])
&& !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'])
&& !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
&& $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
&& $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
) {
return null;
}
if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) {
// Change normal versioned record to delete placeholder
// Happens when an edited record is deleted
GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable($table)
->update(
$table,
['t3ver_state' => VersionState::DELETE_PLACEHOLDER],
['uid' => $id]
);
// Delete localization overlays:
$dataHandler->deleteL10nOverlayRecords($table, $id);
} elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
// Delete those in WS 0 + if their live records state was not "Placeholder".
$dataHandler->deleteEl($table, $id);
if ($recordVersionState->equals(VersionState::MOVE_POINTER)) {
// Delete move-placeholder if current version record is a move-to-pointer.
// deleteEl() can't be used here: The deleteEl() for the MOVE_POINTER record above
// already triggered a delete cascade for children (inline, ...). If we'd
// now call deleteEl() again, we'd trigger adding delete placeholder records for children.
// Thus, it's safe here to just set the MOVE_PLACEHOLDER to deleted (or drop row) straight ahead.
$movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']);
if (!empty($movePlaceholder)) {
$this->softOrHardDeleteSingleRecord($table, (int)$movePlaceholder['uid']);
}
}
} else {
// If live record was placeholder (new/deleted), rather clear
// it from workspace (because it clears both version and placeholder).
$this->version_clearWSID($table, (int)$id, false, $dataHandler);
}
} else {
$dataHandler->newlog('Tried to delete record from another workspace', SystemLogErrorClassification::USER_ERROR);
}
} else {
$dataHandler->newlog('Versioning not enabled for record with an online ID (t3ver_oid) given', SystemLogErrorClassification::SYSTEM_ERROR);
}
} elseif ($dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
// Look, if record is "online" then delete directly.
$dataHandler->deleteEl($table, $id);
} elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) {
// Placeholders for moving operations are deletable directly.
// Get record which its a placeholder for and reset the t3ver_state of that:
if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) {
// Clear the state flag of the workspace version of the record
// Setting placeholder state value for version (so it can know it is currently a new version...)
GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable($table)
->update(
$table,
[
't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE)
],
['uid' => (int)$wsRec['uid']]
);
}
$dataHandler->deleteEl($table, $id);
} else {
// Otherwise, try to delete by versioning:
$copyMappingArray = $dataHandler->copyMappingArray;
$dataHandler->versionizeRecord($table, $id, 'DELETED!', true);
// Determine newly created versions:
// (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
$versionizedElements = ArrayUtility::arrayDiffAssocRecursive($dataHandler->copyMappingArray, $copyMappingArray);
// Delete localization overlays:
foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
$dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
}
}
}
}
/**
* In case a sys_workspace_stage record is deleted we do a hard reset
* for all existing records in that stage to avoid that any of these end up
* as orphan records.
*
* @param string $command
* @param string $table
* @param string $id
* @param string $value
* @param DataHandler $dataHandler
*/
public function processCmdmap_postProcess($command, $table, $id, $value, DataHandler $dataHandler)
{
if ($command === 'delete') {
if ($table === StagesService::TABLE_STAGE) {
$this->resetStageOfElements((int)$id);
} elseif ($table === WorkspaceService::TABLE_WORKSPACE) {
$this->flushWorkspaceElements((int)$id);
$this->emitUpdateTopbarSignal();
}
}
}
public function processDatamap_afterAllOperations(DataHandler $dataHandler): void
{
if (isset($dataHandler->datamap[WorkspaceService::TABLE_WORKSPACE])) {
$this->emitUpdateTopbarSignal();
}
}
/**
* Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about
* moving records that are *not* in the live workspace
*
* @param string $table the table of the record
* @param int $uid the ID of the record
* @param int $destPid Position to move to: $destPid: >=0 then it points to
* @param array $propArr Record properties, like header and pid (includes workspace overlay)
* @param array $moveRec Record properties, like header and pid (without workspace overlay)
* @param int $resolvedPid The final page ID of the record
* @param bool $recordWasMoved can be set so that other hooks or
* @param DataHandler $dataHandler
*/
public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
{
// Only do something in Draft workspace
if ($dataHandler->BE_USER->workspace === 0) {
return;
}
$tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
// Fetch move placeholder, since it might point to a new page in the current workspace
$movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid');
if ($movePlaceHolder !== false && $destPid < 0) {
$resolvedPid = $movePlaceHolder['pid'];
}
$recordWasMoved = true;
$moveRecVersionState = VersionState::cast($moveRec['t3ver_state']);
// Get workspace version of the source record, if any:
$workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
// Handle move-placeholders if the current record is not one already
if (
$tableSupportsVersioning
&& !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER)
) {
// Create version of record first, if it does not exist
if (empty($workspaceVersion['uid'])) {
$dataHandler->versionizeRecord($table, $uid, 'MovePointer');
$workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
if ((int)$resolvedPid !== (int)$propArr['pid']) {
$this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
}
} elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$workspaceVersion['uid']) {
// If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
if ((int)$resolvedPid !== (int)$propArr['pid']) {
$this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
}
}
}
// Check workspace permissions:
$workspaceAccessBlocked = [];
// Element was in "New/Deleted/Moved" so it can be moved...
$recIsNewVersion = $moveRecVersionState->indicatesPlaceholder();
$recordMustNotBeVersionized = $dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table);
$canMoveRecord = $recIsNewVersion || $tableSupportsVersioning;
// Workspace source check:
if (!$recIsNewVersion) {
$errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $workspaceVersion['uid'] ?: $uid);
if ($errorCode) {
$workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
} elseif (!$canMoveRecord && !$recordMustNotBeVersionized) {
$workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
}
}
// Workspace destination check:
// All records can be inserted if $recordMustNotBeVersionized is true.
// Only new versions can be inserted if $recordMustNotBeVersionized is FALSE.
if (!($recordMustNotBeVersionized || $canMoveRecord && !$recordMustNotBeVersionized)) {
$workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
}
if (empty($workspaceAccessBlocked)) {
$versionedRecordUid = (int)$workspaceVersion['uid'];
// moving not needed, just behave like in live workspace
if (!$versionedRecordUid || !$tableSupportsVersioning) {
$recordWasMoved = false;
} elseif ($recIsNewVersion) {
// A newly created record is marked to be moved, so TYPO3 Core is taking care of moving
// the new placeholder.
$recordWasMoved = false;
// However, TYPO3 Core should move the versioned record as well, which is done directly in Core,
// before the placeholder is moved.
$dataHandler->moveRecord_raw($table, $versionedRecordUid, (int)$destPid);
} else {
// If the move operation is done on a versioned record, which is
// NOT new/deleted placeholder, then also create a move placeholder
$this->moveRecord_wsPlaceholders($table, (int)$uid, (int)$destPid, (int)$resolvedPid, $versionedRecordUid, $dataHandler);
}
} else {
$dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), SystemLogErrorClassification::USER_ERROR);
}
}
/**
* Processes fields of a moved record and follows references.
*
* @param DataHandler $dataHandler Calling DataHandler instance
* @param int $resolvedPageId Resolved real destination page id
* @param string $table Name of parent table
* @param int $uid UID of the parent record
*/
protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
{
$versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
if (empty($versionedRecord)) {
return;
}
foreach ($versionedRecord as $field => $value) {
if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
continue;
}
$this->moveRecord_processFieldValue(
$dataHandler,
$resolvedPageId,
$table,
$uid,
$value,
$GLOBALS['TCA'][$table]['columns'][$field]['config']
);
}
}
/**
* Processes a single field of a moved record and follows references.
*
* @param DataHandler $dataHandler Calling DataHandler instance
* @param int $resolvedPageId Resolved real destination page id
* @param string $table Name of parent table
* @param int $uid UID of the parent record
* @param string $value Value of the field of the parent record
* @param array $configuration TCA field configuration of the parent record
*/
protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $value, array $configuration): void
{
$inlineFieldType = $dataHandler->getInlineFieldType($configuration);
$inlineProcessing = (
($inlineFieldType === 'list' || $inlineFieldType === 'field')
&& BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
&& (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
);
if ($inlineProcessing) {
if ($table === 'pages') {
// If the inline elements are related to a page record,
// make sure they reside at that page and not at its parent
$resolvedPageId = $uid;
}
$dbAnalysis = $this->createRelationHandlerInstance();
$dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
// Moving records to a positive destination will insert each
// record at the beginning, thus the order is reversed here:
foreach ($dbAnalysis->itemArray as $item) {
$versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) {
continue;
}
$dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
}
}
}
/****************************
***** Stage Changes ******
****************************/
/**
* Setting stage of record
*
* @param string $table Table name
* @param int $id
* @param int $stageId Stage ID to set
* @param string $comment Comment that goes into log
* @param DataHandler $dataHandler DataHandler object
* @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
*/
protected function version_setStage($table, $id, $stageId, string $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
{
if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
$dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR);
} elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) {
$record = BackendUtility::getRecord($table, $id);
$workspaceInfo = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']);
// check if the user is allowed to the current stage, so it's also allowed to send to next stage
if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
// Set stage of record:
GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable($table)
->update(
$table,
[
't3ver_stage' => $stageId,
],
['uid' => (int)$id]
);
if ($dataHandler->enableLogging) {
$propertyArray = $dataHandler->getRecordProperties($table, $id);
$pid = $propertyArray['pid'];
$dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
}
// TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
$dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]);
if ((int)$workspaceInfo['stagechg_notification'] > 0) {
$this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$workspaceInfo, $stageId, $comment];
$this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = [$table, $id];
$this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['recipients'] = $notificationAlternativeRecipients;
}
} else {
$dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', SystemLogErrorClassification::USER_ERROR);
}
} else {
$dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', SystemLogErrorClassification::USER_ERROR);
}
}
/*****************************
***** CMD versioning ******
*****************************/
/**
* Publishing / Swapping (= switching) versions of a record
* 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
*
* @param string $table Table name
* @param int $id UID of the online record to swap
* @param int $swapWith UID of the archived version to swap with!
* @param DataHandler $dataHandler DataHandler object
* @param string $comment Notification comment
* @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
*/
protected function version_swap($table, $id, $swapWith, DataHandler $dataHandler, string $comment, $notificationAlternativeRecipients = [])
{
// Check prerequisites before start publishing
// Skip records that have been deleted during the current execution
if ($dataHandler->hasDeletedRecord($table, $id)) {
return;
}
// First, check if we may actually edit the online record
if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
$dataHandler->newlog(
sprintf(
'Error: You cannot swap versions for record %s:%d you do not have access to edit!',
$table,
$id
),
SystemLogErrorClassification::USER_ERROR
);
return;
}
// Select the two versions:
$curVersion = BackendUtility::getRecord($table, $id, '*');
$swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
$movePlh = [];
$movePlhID = 0;
if (!(is_array($curVersion) && is_array($swapVersion))) {
$dataHandler->newlog(
sprintf(
'Error: Either online or swap version for %s:%d->%d could not be selected!',
$table,
$id,
$swapWith
),
SystemLogErrorClassification::SYSTEM_ERROR
);
return;
}
if (!$dataHandler->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
$dataHandler->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], SystemLogErrorClassification::USER_ERROR);
return;
}
$wsAccess = $dataHandler->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']);
if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10)) {
$dataHandler->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', SystemLogErrorClassification::USER_ERROR);
return;
}
if (!($dataHandler->doesRecordExist($table, $swapWith, Permission::PAGE_SHOW) && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) {
$dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', SystemLogErrorClassification::USER_ERROR);
return;
}
// Check if the swapWith record really IS a version of the original!
if (!(((int)$swapVersion['t3ver_oid'] > 0 && (int)$curVersion['t3ver_oid'] === 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
$dataHandler->newlog('In offline record, either t3ver_oid was not set or the t3ver_oid didn\'t match the id of the online version as it must!', SystemLogErrorClassification::SYSTEM_ERROR);
return;
}
// Lock file name:
$lockFileName = Environment::getVarPath() . '/lock/workspaces_publish' . $table . '_' . $id . '.json';
if (@is_file($lockFileName)) {
$lockFileContents = file_get_contents($lockFileName);
$lockFileContents = json_decode($lockFileContents ?: '', true);
// Only skip if the lock file is newer than the last 1h (a publishing process should not be running longer than 60mins)
if (isset($lockFileContents['tstamp']) && $lockFileContents['tstamp'] > ($GLOBALS['EXEC_TIME']-3600)) {
$dataHandler->newlog('A publishing lock file was present. Either another publish process is already running or a previous publish process failed. Ask your administrator to handle the situation.', SystemLogErrorClassification::SYSTEM_ERROR);
return;
}
}
// Now start to publishing records by first creating the lock file
// Write lock-file:
GeneralUtility::writeFileToTypo3tempDir($lockFileName, (string)json_encode([
'tstamp' => $GLOBALS['EXEC_TIME'],
'user' => $dataHandler->BE_USER->user['username'],
'curVersion' => $curVersion,
'swapVersion' => $swapVersion
]));
// Find fields to keep
$keepFields = $this->getUniqueFields($table);
if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
$keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
}
// l10n-fields must be kept otherwise the localization
// will be lost during the publishing
if ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
$keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
}
// Swap "keepfields"
foreach ($keepFields as $fN) {
$tmp = $swapVersion[$fN];
$swapVersion[$fN] = $curVersion[$fN];
$curVersion[$fN] = $tmp;
}
// Preserve states:
$t3ver_state = [];
$t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
// Modify offline version to become online:
// Set pid for ONLINE
$swapVersion['pid'] = (int)$curVersion['pid'];
// We clear this because t3ver_oid only make sense for offline versions
// and we want to prevent unintentional misuse of this
// value for online records.
$swapVersion['t3ver_oid'] = 0;
// In case of swapping and the offline record has a state
// (like 2 or 4 for deleting or move-pointer) we set the
// current workspace ID so the record is not deselected
// in the interface by BackendUtility::versioningPlaceholderClause()
$swapVersion['t3ver_wsid'] = 0;
$swapVersion['t3ver_stage'] = 0;
$swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
// Moving element.
if (BackendUtility::isTableWorkspaceEnabled($table)) {
// && $t3ver_state['swapVersion']==4 // Maybe we don't need this?
if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) {
$movePlhID = $plhRec['uid'];
$movePlh['pid'] = $swapVersion['pid'];
$swapVersion['pid'] = (int)$plhRec['pid'];
$curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state'];
$swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
// sortby is a "keepFields" which is why this will work...
$movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
$swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
}
}
}
// Take care of relations in each field (e.g. IRRE):
if (is_array($GLOBALS['TCA'][$table]['columns'])) {
foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
if (isset($fieldConf['config']) && is_array($fieldConf['config'])) {
$this->version_swap_processFields($table, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler);
}
}
}
unset($swapVersion['uid']);
// Modify online version to become offline:
unset($curVersion['uid']);
// Mark curVersion to contain the oid
$curVersion['t3ver_oid'] = (int)$id;
$curVersion['t3ver_wsid'] = 0;
// Increment lifecycle counter
$curVersion['t3ver_stage'] = 0;
$curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
// Registering and swapping MM relations in current and swap records:
$dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith);
// Generating proper history data to prepare logging
$dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
$dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
// Execute swapping:
$sqlErrors = [];
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
$platform = $connection->getDatabasePlatform();
$tableDetails = null;
if ($platform instanceof SQLServerPlatform) {
// mssql needs to set proper PARAM_LOB and others to update fields
$tableDetails = $connection->getSchemaManager()->listTableDetails($table);
}
try {
$types = [];
if ($platform instanceof SQLServerPlatform) {
foreach ($curVersion as $columnName => $columnValue) {
$types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
}
}
$connection->update(
$table,
$swapVersion,
['uid' => (int)$id],
$types
);
} catch (DBALException $e) {
$sqlErrors[] = $e->getPrevious()->getMessage();
}
if (empty($sqlErrors)) {
try {
$types = [];
if ($platform instanceof SQLServerPlatform) {
foreach ($curVersion as $columnName => $columnValue) {
$types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
}
}
$connection->update(
$table,
$curVersion,
['uid' => (int)$swapWith],
$types
);
unlink($lockFileName);
} catch (DBALException $e) {
$sqlErrors[] = $e->getPrevious()->getMessage();
}
}
if (!empty($sqlErrors)) {
$dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), SystemLogErrorClassification::SYSTEM_ERROR);
} else {
// Register swapped ids for later remapping:
$this->remappedIds[$table][$id] = $swapWith;
$this->remappedIds[$table][$swapWith] = $id;
// If a moving operation took place...:
if ($movePlhID) {
// Remove, if normal publishing:
// For delete + completely delete!
$dataHandler->deleteEl($table, $movePlhID, true, true);
}
// Checking for delete:
// Delete only if new/deleted placeholders are there.
if (((int)$t3ver_state['swapVersion'] === VersionState::NEW_PLACEHOLDER || (int)$t3ver_state['swapVersion'] === VersionState::DELETE_PLACEHOLDER)) {
// Force delete
$dataHandler->deleteEl($table, $id, true);
}
if ($dataHandler->enableLogging) {
$dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Publishing successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, -1, [], $dataHandler->eventPid($table, $id, $swapVersion['pid']));
}
// Update reference index of the live record:
$dataHandler->addRemapStackRefIndex($table, $id);
// Set log entry for live record:
$propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion);
if (($propArr['t3ver_oid'] ?? 0) > 0) {
$label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
} else {
$label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
}
$theLogId = $dataHandler->log($table, $id, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
$dataHandler->setHistory($table, $id, $theLogId);
// Update reference index of the offline record:
$dataHandler->addRemapStackRefIndex($table, $swapWith);
// Set log entry for offline record:
$propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion);
if (($propArr['t3ver_oid'] ?? 0) > 0) {
$label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
} else {
$label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
}
$theLogId = $dataHandler->log($table, $swapWith, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']);
$dataHandler->setHistory($table, $swapWith, $theLogId);
$stageId = StagesService::STAGE_PUBLISH_EXECUTE_ID;
$notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
$this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
$this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id];
$this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients;
// Write to log with stageId -20 (STAGE_PUBLISH_EXECUTE_ID)
if ($dataHandler->enableLogging) {
$propArr = $dataHandler->getRecordProperties($table, $id);
$pid = $propArr['pid'];
$dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
}
$dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
// Clear cache:
$dataHandler->registerRecordIdForPageCacheClearing($table, $id);
// If published, delete the record from the database
if ($table === 'pages') {
// Note on fifth argument false: At this point both $curVersion and $swapVersion page records are
// identical in DB. deleteEl() would now usually find all records assigned to our obsolete
// page which at the same time belong to our current version page, and would delete them.
// To suppress this, false tells deleteEl() to only delete the obsolete page but not its assigned records.
$dataHandler->deleteEl($table, $swapWith, true, true, false);
} else {
$dataHandler->deleteEl($table, $swapWith, true, true);
}
// Update reference index for live workspace too:
/** @var \TYPO3\CMS\Core\Database\ReferenceIndex $refIndexObj */
$refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
$refIndexObj->setWorkspaceId(0);
$refIndexObj->updateRefIndexTable($table, $id);
$refIndexObj->updateRefIndexTable($table, $swapWith);
}
}
/**
* Processes fields of a record for the publishing/swapping process.
* Basically this takes care of IRRE (type "inline") child references.
*
* @param string $tableName Table name
* @param array $configuration TCA field configuration
* @param array $liveData Live record data
* @param array $versionData Version record data
* @param DataHandler $dataHandler Calling data-handler object
*/
protected function version_swap_processFields($tableName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
{
$inlineType = $dataHandler->getInlineFieldType($configuration);
if ($inlineType !== 'field') {
return;
}
$foreignTable = $configuration['foreign_table'];
// Read relations that point to the current record (e.g. live record):
$liveRelations = $this->createRelationHandlerInstance();
$liveRelations->setWorkspaceId(0);
$liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration);
// Read relations that point to the record to be swapped with e.g. draft record):
$versionRelations = $this->createRelationHandlerInstance();
$versionRelations->setUseLiveReferenceIds(false);
$versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration);
// Update relations for both (workspace/versioning) sites:
if (!empty($liveRelations->itemArray)) {
$dataHandler->addRemapAction(
$tableName,
$liveData['uid'],
[$this, 'updateInlineForeignFieldSorting'],
[$liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace]
);
}
if (!empty($versionRelations->itemArray)) {
$dataHandler->addRemapAction(
$tableName,
$liveData['uid'],
[$this, 'updateInlineForeignFieldSorting'],
[$liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0]
);
}
}
/**
* Updates foreign field sorting values of versioned and live
* parents after(!) the whole structure has been published.
*
* This method is used as callback function in
* DataHandlerHook::version_swap_procBasedOnFieldType().
* Sorting fields ("sortby") are not modified during the
* workspace publishing/swapping process directly.
*
* @param string $parentId
* @param string $foreignTableName
* @param int[] $foreignIds
* @param array $configuration
* @param int $targetWorkspaceId
* @internal
*/
public function updateInlineForeignFieldSorting($parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
{
$remappedIds = [];
// Use remapped ids (live id <-> version id)
foreach ($foreignIds as $foreignId) {
if (!empty($this->remappedIds[$foreignTableName][$foreignId])) {
$remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId];
} else {
$remappedIds[] = $foreignId;
}
}
$relationHandler = $this->createRelationHandlerInstance();
$relationHandler->setWorkspaceId($targetWorkspaceId);
$relationHandler->setUseLiveReferenceIds(false);
$relationHandler->start(implode(',', $remappedIds), $foreignTableName);
$relationHandler->processDeletePlaceholder();
$relationHandler->writeForeignField($configuration, $parentId);
}
/**
* Remove a versioned record from this workspace. Often referred to as "discarding a version" = throwing away a version.
* This means to delete the record and remove any placeholders that are not needed anymore.
*
* In previous versions, this meant that the versioned record was marked as deleted and moved into "live" workspace.
*
* @param string $table Database table name
* @param int $versionId Version record uid
* @param bool $flush If set, will completely delete element
* @param DataHandler $dataHandler DataHandler object
*/
protected function version_clearWSID(string $table, int $versionId, bool $flush, DataHandler $dataHandler): void
{
if ($dataHandler->hasDeletedRecord($table, $versionId)) {
// If discarding pages and records at once, deleting the page record may have already deleted
// records on the page, rendering a call to delete single elements of this page bogus. The
// data handler tracks which records have been deleted in the same process, so ignore
// the record in question if its in the list.
return;
}
if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $versionId)) {
$dataHandler->newlog('Attempt to reset workspace for record ' . $table . ':' . $versionId . ' failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR);
return;
}
if (!$dataHandler->checkRecordUpdateAccess($table, $versionId)) {
$dataHandler->newlog('Attempt to reset workspace for record ' . $table . ':' . $versionId . ' failed because you do not have edit access', SystemLogErrorClassification::USER_ERROR);
return;
}
$liveRecord = BackendUtility::getLiveVersionOfRecord($table, $versionId, 'uid,t3ver_state');
if (!$liveRecord) {
// Attempting to discard a record that has no live version, don't do anything
return;
}
$liveState = VersionState::cast($liveRecord['t3ver_state']);
$versionRecord = BackendUtility::getRecord($table, $versionId);
$versionState = VersionState::cast($versionRecord['t3ver_state']);
$deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
if ($flush || $versionState->equals(VersionState::DELETE_PLACEHOLDER)) {
// Purge delete placeholder since it would not contain any modified information
$dataHandler->deleteEl($table, $versionRecord['uid'], true, true);
} elseif ($deleteField === null) {
// let DataHandler decide how to delete the record that does not have a deleted field
$dataHandler->deleteEl($table, $versionRecord['uid'], true);
} else {
// update record directly in order to avoid delete cascades on this version
$this->softOrHardDeleteSingleRecord($table, (int)$versionId);
$dataHandler->updateRefIndex($table, (int)$versionId);
}
if ($versionState->equals(VersionState::MOVE_POINTER)) {
// purge move placeholder as it has been created just for the sake of pointing to a version
$movePlaceHolderRecord = BackendUtility::getMovePlaceholder($table, $liveRecord['uid'], 'uid');
if (is_array($movePlaceHolderRecord)) {
$dataHandler->deleteEl($table, (int)$movePlaceHolderRecord['uid'], true, $flush);
}
} elseif ($liveState->equals(VersionState::NEW_PLACEHOLDER)) {
// purge new placeholder as it has been created just for the sake of pointing to a version
// THIS assumes that the record was placeholder ONLY for ONE record (namely $id)
$dataHandler->deleteEl($table, $liveRecord['uid'], true, $flush);
}
}
/**