Passed
Push — master ( 17f551...b02172 )
by
unknown
16:39
created

DataHandler::deleteSpecificPage()   C

Complexity

Conditions 12
Paths 5

Size

Total Lines 85
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 45
nc 5
nop 3
dl 0
loc 85
rs 6.9666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\DataHandling;
17
18
use Doctrine\DBAL\DBALException;
19
use Doctrine\DBAL\Driver\Statement;
20
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
21
use Doctrine\DBAL\Platforms\SQLServerPlatform;
22
use Doctrine\DBAL\Types\IntegerType;
23
use Psr\Log\LoggerAwareInterface;
24
use Psr\Log\LoggerAwareTrait;
25
use TYPO3\CMS\Backend\Utility\BackendUtility;
26
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27
use TYPO3\CMS\Core\Cache\CacheManager;
28
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
29
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException;
30
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowException;
31
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowLoopException;
32
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowRootException;
33
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidPointerFieldValueException;
34
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
35
use TYPO3\CMS\Core\Configuration\Richtext;
36
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
37
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
38
use TYPO3\CMS\Core\Database\Connection;
39
use TYPO3\CMS\Core\Database\ConnectionPool;
40
use TYPO3\CMS\Core\Database\Query\QueryHelper;
41
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
42
use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface;
43
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
44
use TYPO3\CMS\Core\Database\RelationHandler;
45
use TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore;
46
use TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor;
47
use TYPO3\CMS\Core\DataHandling\Model\CorrelationId;
48
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
49
use TYPO3\CMS\Core\Html\RteHtmlParser;
50
use TYPO3\CMS\Core\Localization\LanguageService;
51
use TYPO3\CMS\Core\Messaging\FlashMessage;
52
use TYPO3\CMS\Core\Messaging\FlashMessageService;
53
use TYPO3\CMS\Core\Resource\ResourceFactory;
54
use TYPO3\CMS\Core\Service\OpcodeCacheService;
55
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
56
use TYPO3\CMS\Core\SysLog\Action\Cache as SystemLogCacheAction;
57
use TYPO3\CMS\Core\SysLog\Action\Database as SystemLogDatabaseAction;
58
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
59
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
60
use TYPO3\CMS\Core\Type\Bitmask\Permission;
61
use TYPO3\CMS\Core\Utility\ArrayUtility;
62
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
63
use TYPO3\CMS\Core\Utility\GeneralUtility;
64
use TYPO3\CMS\Core\Utility\HttpUtility;
65
use TYPO3\CMS\Core\Utility\MathUtility;
66
use TYPO3\CMS\Core\Utility\StringUtility;
67
use TYPO3\CMS\Core\Versioning\VersionState;
68
69
/**
70
 * The main data handler class which takes care of correctly updating and inserting records.
71
 * This class was formerly known as TCEmain.
72
 *
73
 * This is the TYPO3 Core Engine class for manipulation of the database
74
 * This class is used by eg. the tce_db BE route (SimpleDataHandlerController) which provides an interface for POST forms to this class.
75
 *
76
 * Dependencies:
77
 * - $GLOBALS['TCA'] must exist
78
 * - $GLOBALS['LANG'] must exist
79
 *
80
 * Also see document 'TYPO3 Core API' for details.
81
 */
82
class DataHandler implements LoggerAwareInterface
83
{
84
    use LoggerAwareTrait;
85
86
    // *********************
87
    // Public variables you can configure before using the class:
88
    // *********************
89
    /**
90
     * If TRUE, the default log-messages will be stored. This should not be necessary if the locallang-file for the
91
     * log-display is properly configured. So disabling this will just save some database-space as the default messages are not saved.
92
     *
93
     * @var bool
94
     */
95
    public $storeLogMessages = true;
96
97
    /**
98
     * If TRUE, actions are logged to sys_log.
99
     *
100
     * @var bool
101
     */
102
    public $enableLogging = true;
103
104
    /**
105
     * If TRUE, the datamap array is reversed in the order, which is a nice thing if you're creating a whole new
106
     * bunch of records.
107
     *
108
     * @var bool
109
     */
110
    public $reverseOrder = false;
111
112
    /**
113
     * If TRUE, only fields which are different from the database values are saved! In fact, if a whole input array
114
     * is similar, it's not saved then.
115
     *
116
     * @var bool
117
     * @internal should only be used from within TYPO3 Core
118
     */
119
    public $checkSimilar = true;
120
121
    /**
122
     * This will read the record after having updated or inserted it. If anything is not properly submitted an error
123
     * is written to the log. This feature consumes extra time by selecting records
124
     *
125
     * @var bool
126
     */
127
    public $checkStoredRecords = true;
128
129
    /**
130
     * If set, values '' and 0 will equal each other when the stored records are checked.
131
     *
132
     * @var bool
133
     */
134
    public $checkStoredRecords_loose = true;
135
136
    /**
137
     * If set, then the 'hideAtCopy' flag for tables will be ignored.
138
     *
139
     * @var bool
140
     */
141
    public $neverHideAtCopy = false;
142
143
    /**
144
     * If set, then the TCE class has been instantiated during an import action of a T3D
145
     *
146
     * @var bool
147
     */
148
    public $isImporting = false;
149
150
    /**
151
     * If set, then transformations are NOT performed on the input.
152
     *
153
     * @var bool
154
     */
155
    public $dontProcessTransformations = false;
156
157
    /**
158
     * Will distinguish between translations (with parent) and localizations (without parent) while still using the same methods to copy the records
159
     * TRUE: translation of a record connected to the default language
160
     * FALSE: localization of a record without connection to the default language
161
     *
162
     * @var bool
163
     */
164
    protected $useTransOrigPointerField = true;
165
166
    /**
167
     * If TRUE, workspace restrictions are bypassed on edit and create actions (process_datamap()).
168
     * YOU MUST KNOW what you do if you use this feature!
169
     *
170
     * @var bool
171
     * @internal should only be used from within TYPO3 Core
172
     */
173
    public $bypassWorkspaceRestrictions = false;
174
175
    /**
176
     * If TRUE, access check, check for deleted etc. for records is bypassed.
177
     * YOU MUST KNOW what you are doing if you use this feature!
178
     *
179
     * @var bool
180
     */
181
    public $bypassAccessCheckForRecords = false;
182
183
    /**
184
     * Comma-separated list. This list of tables decides which tables will be copied. If empty then none will.
185
     * If '*' then all will (that the user has permission to of course)
186
     *
187
     * @var string
188
     * @internal should only be used from within TYPO3 Core
189
     */
190
    public $copyWhichTables = '*';
191
192
    /**
193
     * If 0 then branch is NOT copied.
194
     * If 1 then pages on the 1st level is copied.
195
     * If 2 then pages on the second level is copied ... and so on
196
     *
197
     * @var int
198
     */
199
    public $copyTree = 0;
200
201
    /**
202
     * [table][fields]=value: New records are created with default values and you can set this array on the
203
     * form $defaultValues[$table][$field] = $value to override the default values fetched from TCA.
204
     * If ->setDefaultsFromUserTS is called UserTSconfig default values will overrule existing values in this array
205
     * (thus UserTSconfig overrules externally set defaults which overrules TCA defaults)
206
     *
207
     * @var array
208
     * @internal should only be used from within TYPO3 Core
209
     */
210
    public $defaultValues = [];
211
212
    /**
213
     * [table][fields]=value: You can set this array on the form $overrideValues[$table][$field] = $value to
214
     * override the incoming data. You must set this externally. You must make sure the fields in this array are also
215
     * found in the table, because it's not checked. All columns can be set by this array!
216
     *
217
     * @var array
218
     * @internal should only be used from within TYPO3 Core
219
     */
220
    public $overrideValues = [];
221
222
    /**
223
     * If entries are set in this array corresponding to fields for update, they are ignored and thus NOT updated.
224
     * You could set this array from a series of checkboxes with value=0 and hidden fields before the checkbox with 1.
225
     * Then an empty checkbox will disable the field.
226
     *
227
     * @var array
228
     * @internal should only be used from within TYPO3 Core
229
     */
230
    public $data_disableFields = [];
231
232
    /**
233
     * Use this array to validate suggested uids for tables by setting [table]:[uid]. This is a dangerous option
234
     * since it will force the inserted record to have a certain UID. The value just have to be TRUE, but if you set
235
     * it to "DELETE" it will make sure any record with that UID will be deleted first (raw delete).
236
     * The option is used for import of T3D files when synchronizing between two mirrored servers.
237
     * As a security measure this feature is available only for Admin Users (for now)
238
     *
239
     * @var array
240
     */
241
    public $suggestedInsertUids = [];
242
243
    /**
244
     * Object. Call back object for FlexForm traversal. Useful when external classes wants to use the
245
     * iteration functions inside DataHandler for traversing a FlexForm structure.
246
     *
247
     * @var object
248
     * @internal should only be used from within TYPO3 Core
249
     */
250
    public $callBackObj;
251
252
    /**
253
     * A string which can be used as correlationId for RecordHistory entries.
254
     * The string can later be used to rollback multiple changes at once.
255
     *
256
     * @var CorrelationId|null
257
     */
258
    protected $correlationId;
259
260
    // *********************
261
    // Internal variables (mapping arrays) which can be used (read-only) from outside
262
    // *********************
263
    /**
264
     * Contains mapping of auto-versionized records.
265
     *
266
     * @var array
267
     * @internal should only be used from within TYPO3 Core
268
     */
269
    public $autoVersionIdMap = [];
270
271
    /**
272
     * When new elements are created, this array contains a map between their "NEW..." string IDs and the eventual UID they got when stored in database
273
     *
274
     * @var array
275
     */
276
    public $substNEWwithIDs = [];
277
278
    /**
279
     * Like $substNEWwithIDs, but where each old "NEW..." id is mapped to the table it was from.
280
     *
281
     * @var array
282
     * @internal should only be used from within TYPO3 Core
283
     */
284
    public $substNEWwithIDs_table = [];
285
286
    /**
287
     * Holds the tables and there the ids of newly created child records from IRRE
288
     *
289
     * @var array
290
     * @internal should only be used from within TYPO3 Core
291
     */
292
    public $newRelatedIDs = [];
293
294
    /**
295
     * This array is the sum of all copying operations in this class.
296
     *
297
     * @var array
298
     * @internal should only be used from within TYPO3 Core
299
     */
300
    public $copyMappingArray_merged = [];
301
302
    /**
303
     * Per-table array with UIDs that have been deleted.
304
     *
305
     * @var array
306
     */
307
    protected $deletedRecords = [];
308
309
    /**
310
     * Errors are collected in this variable.
311
     *
312
     * @var array
313
     * @internal should only be used from within TYPO3 Core
314
     */
315
    public $errorLog = [];
316
317
    /**
318
     * Fields from the pages-table for which changes will trigger a pagetree refresh
319
     *
320
     * @var array
321
     */
322
    public $pagetreeRefreshFieldsFromPages = ['pid', 'sorting', 'deleted', 'hidden', 'title', 'doktype', 'is_siteroot', 'fe_group', 'nav_hide', 'nav_title', 'module', 'starttime', 'endtime', 'content_from_pid', 'extendToSubpages'];
323
324
    /**
325
     * Indicates whether the pagetree needs a refresh because of important changes
326
     *
327
     * @var bool
328
     * @internal should only be used from within TYPO3 Core
329
     */
330
    public $pagetreeNeedsRefresh = false;
331
332
    // *********************
333
    // Internal Variables, do not touch.
334
    // *********************
335
336
    // Variables set in init() function:
337
338
    /**
339
     * The user-object the script uses. If not set from outside, this is set to the current global $BE_USER.
340
     *
341
     * @var BackendUserAuthentication
342
     */
343
    public $BE_USER;
344
345
    /**
346
     * Will be set to uid of be_user executing this script
347
     *
348
     * @var int
349
     * @internal should only be used from within TYPO3 Core
350
     */
351
    public $userid;
352
353
    /**
354
     * Will be set to username of be_user executing this script
355
     *
356
     * @var string
357
     * @internal should only be used from within TYPO3 Core
358
     */
359
    public $username;
360
361
    /**
362
     * Will be set if user is admin
363
     *
364
     * @var bool
365
     * @internal should only be used from within TYPO3 Core
366
     */
367
    public $admin;
368
369
    /**
370
     * @var PagePermissionAssembler
371
     */
372
    protected $pagePermissionAssembler;
373
374
    /**
375
     * The list of <table>-<fields> that cannot be edited by user. This is compiled from TCA/exclude-flag combined with non_exclude_fields for the user.
376
     *
377
     * @var array
378
     */
379
    protected $excludedTablesAndFields = [];
380
381
    /**
382
     * Data submitted from the form view, used to control behaviours,
383
     * e.g. this is used to activate/deactivate fields and thus store NULL values
384
     *
385
     * @var array
386
     */
387
    protected $control = [];
388
389
    /**
390
     * Set with incoming data array
391
     *
392
     * @var array
393
     */
394
    public $datamap = [];
395
396
    /**
397
     * Set with incoming cmd array
398
     *
399
     * @var array
400
     */
401
    public $cmdmap = [];
402
403
    /**
404
     * List of changed old record ids to new records ids
405
     *
406
     * @var array
407
     */
408
    protected $mmHistoryRecords = [];
409
410
    /**
411
     * List of changed old record ids to new records ids
412
     *
413
     * @var array
414
     */
415
    protected $historyRecords = [];
416
417
    // Internal static:
418
419
    /**
420
     * The interval between sorting numbers used with tables with a 'sorting' field defined.
421
     *
422
     * Min 1, should be power of 2
423
     *
424
     * @var int
425
     * @internal should only be used from within TYPO3 Core
426
     */
427
    public $sortIntervals = 256;
428
429
    // Internal caching arrays
430
    /**
431
     * User by function checkRecordInsertAccess() to store whether a record can be inserted on a page id
432
     *
433
     * @var array
434
     */
435
    protected $recInsertAccessCache = [];
436
437
    /**
438
     * Caching array for check of whether records are in a webmount
439
     *
440
     * @var array
441
     */
442
    protected $isRecordInWebMount_Cache = [];
443
444
    /**
445
     * Caching array for page ids in webmounts
446
     *
447
     * @var array
448
     */
449
    protected $isInWebMount_Cache = [];
450
451
    /**
452
     * Used for caching page records in pageInfo()
453
     *
454
     * @var array
455
     */
456
    protected $pageCache = [];
457
458
    // Other arrays:
459
    /**
460
     * For accumulation of MM relations that must be written after new records are created.
461
     *
462
     * @var array
463
     * @internal
464
     */
465
    public $dbAnalysisStore = [];
466
467
    /**
468
     * Used for tracking references that might need correction after operations
469
     *
470
     * @var array
471
     * @internal
472
     */
473
    public $registerDBList = [];
474
475
    /**
476
     * Used for tracking references that might need correction in pid field after operations (e.g. IRRE)
477
     *
478
     * @var array
479
     * @internal
480
     */
481
    public $registerDBPids = [];
482
483
    /**
484
     * Used by the copy action to track the ids of new pages so subpages are correctly inserted!
485
     * THIS is internally cleared for each executed copy operation! DO NOT USE THIS FROM OUTSIDE!
486
     * Read from copyMappingArray_merged instead which is accumulating this information.
487
     *
488
     * NOTE: This is used by some outside scripts (e.g. hooks), as the results in $copyMappingArray_merged
489
     * are only available after an action has been completed.
490
     *
491
     * @var array
492
     * @internal
493
     */
494
    public $copyMappingArray = [];
495
496
    /**
497
     * Array used for remapping uids and values at the end of process_datamap
498
     *
499
     * @var array
500
     * @internal
501
     */
502
    public $remapStack = [];
503
504
    /**
505
     * Array used for remapping uids and values at the end of process_datamap
506
     * (e.g. $remapStackRecords[<table>][<uid>] = <index in $remapStack>)
507
     *
508
     * @var array
509
     * @internal
510
     */
511
    public $remapStackRecords = [];
512
513
    /**
514
     * Array used for checking whether new children need to be remapped
515
     *
516
     * @var array
517
     */
518
    protected $remapStackChildIds = [];
519
520
    /**
521
     * Array used for executing addition actions after remapping happened (set processRemapStack())
522
     *
523
     * @var array
524
     */
525
    protected $remapStackActions = [];
526
527
    /**
528
     * Registry object to gather reference index update requests and perform updates after
529
     * main processing has been done. The first call to start() instantiates this object.
530
     * Recursive sub instances receive this instance via __construct().
531
     * The final update() call is done at the end of process_cmdmap() or process_datamap()
532
     * in the outer most instance.
533
     *
534
     * @var ReferenceIndexUpdater
535
     */
536
    protected $referenceIndexUpdater;
537
538
    /**
539
     * Tells, that this DataHandler instance was called from \TYPO3\CMS\Impext\ImportExport.
540
     * This variable is set by \TYPO3\CMS\Impext\ImportExport
541
     *
542
     * @var bool
543
     * @internal only used within TYPO3 Core
544
     */
545
    public $callFromImpExp = false;
546
547
    // Various
548
549
    /**
550
     * Set to "currentRecord" during checking of values.
551
     *
552
     * @var array
553
     * @internal
554
     */
555
    public $checkValue_currentRecord = [];
556
557
    /**
558
     * Disable delete clause
559
     *
560
     * @var bool
561
     */
562
    protected $disableDeleteClause = false;
563
564
    /**
565
     * @var array
566
     */
567
    protected $checkModifyAccessListHookObjects;
568
569
    /**
570
     * @var array
571
     */
572
    protected $version_remapMMForVersionSwap_reg;
573
574
    /**
575
     * The outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler:
576
     * This object instantiates itself on versioning and localization ...
577
     *
578
     * @var \TYPO3\CMS\Core\DataHandling\DataHandler
579
     */
580
    protected $outerMostInstance;
581
582
    /**
583
     * Internal cache for collecting records that should trigger cache clearing
584
     *
585
     * @var array
586
     */
587
    protected static $recordsToClearCacheFor = [];
588
589
    /**
590
     * Internal cache for pids of records which were deleted. It's not possible
591
     * to retrieve the parent folder/page at a later stage
592
     *
593
     * @var array
594
     */
595
    protected static $recordPidsForDeletedRecords = [];
596
597
    /**
598
     * Runtime Cache to store and retrieve data computed for a single request
599
     *
600
     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
601
     */
602
    protected $runtimeCache;
603
604
    /**
605
     * Prefix for the cache entries of nested element calls since the runtimeCache has a global scope.
606
     *
607
     * @var string
608
     */
609
    protected $cachePrefixNestedElementCalls = 'core-datahandler-nestedElementCalls-';
610
611
    /**
612
     * Sets up the data handler cache and some additional options, the main logic is done in the start() method.
613
     *
614
     * @param ReferenceIndexUpdater|null $referenceIndexUpdater Hand over from outer most instance to sub instances
615
     */
616
    public function __construct(ReferenceIndexUpdater $referenceIndexUpdater = null)
617
    {
618
        $this->checkStoredRecords = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecords'];
619
        $this->checkStoredRecords_loose = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecordsLoose'];
620
        $this->runtimeCache = $this->getRuntimeCache();
621
        $this->pagePermissionAssembler = GeneralUtility::makeInstance(PagePermissionAssembler::class, $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions']);
622
        if ($referenceIndexUpdater === null) {
623
            // Create ReferenceIndexUpdater object. This should only happen on outer most instance,
624
            // sub instances should receive the reference index updater from a parent.
625
            $referenceIndexUpdater = GeneralUtility::makeInstance(ReferenceIndexUpdater::class);
626
        }
627
        $this->referenceIndexUpdater = $referenceIndexUpdater;
628
    }
629
630
    /**
631
     * @param array $control
632
     * @internal
633
     */
634
    public function setControl(array $control)
635
    {
636
        $this->control = $control;
637
    }
638
639
    /**
640
     * Initializing.
641
     * For details, see 'TYPO3 Core API' document.
642
     * This function does not start the processing of data, but merely initializes the object
643
     *
644
     * @param array $data Data to be modified or inserted in the database
645
     * @param array $cmd Commands to copy, move, delete, localize, versionize records.
646
     * @param BackendUserAuthentication|null $altUserObject An alternative userobject you can set instead of the default, which is $GLOBALS['BE_USER']
647
     */
648
    public function start($data, $cmd, $altUserObject = null)
649
    {
650
        // Initializing BE_USER
651
        $this->BE_USER = is_object($altUserObject) ? $altUserObject : $GLOBALS['BE_USER'];
652
        $this->userid = $this->BE_USER->user['uid'] ?? 0;
653
        $this->username = $this->BE_USER->user['username'] ?? '';
654
        $this->admin = $this->BE_USER->user['admin'] ?? false;
655
656
        // set correlation id for each new set of data or commands
657
        $this->correlationId = CorrelationId::forScope(
658
            md5(StringUtility::getUniqueId(self::class))
659
        );
660
661
        // Get default values from user TSconfig
662
        $tcaDefaultOverride = $this->BE_USER->getTSConfig()['TCAdefaults.'] ?? null;
663
        if (is_array($tcaDefaultOverride)) {
664
            $this->setDefaultsFromUserTS($tcaDefaultOverride);
665
        }
666
667
        // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
668
        if (!$this->admin) {
669
            $this->excludedTablesAndFields = array_flip($this->getExcludeListArray());
670
        }
671
        // Setting the data and cmd arrays
672
        if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
673
            reset($data);
674
            $this->datamap = $data;
675
        }
676
        if (is_array($cmd)) {
0 ignored issues
show
introduced by
The condition is_array($cmd) is always true.
Loading history...
677
            reset($cmd);
678
            $this->cmdmap = $cmd;
679
        }
680
    }
681
682
    /**
683
     * Function that can mirror input values in datamap-array to other uid numbers.
684
     * Example: $mirror[table][11] = '22,33' will look for content in $this->datamap[table][11] and copy it to $this->datamap[table][22] and $this->datamap[table][33]
685
     *
686
     * @param array $mirror This array has the syntax $mirror[table_name][uid] = [list of uids to copy data-value TO!]
687
     * @internal
688
     */
689
    public function setMirror($mirror)
690
    {
691
        if (!is_array($mirror)) {
0 ignored issues
show
introduced by
The condition is_array($mirror) is always true.
Loading history...
692
            return;
693
        }
694
695
        foreach ($mirror as $table => $uid_array) {
696
            if (!isset($this->datamap[$table])) {
697
                continue;
698
            }
699
700
            foreach ($uid_array as $id => $uidList) {
701
                if (!isset($this->datamap[$table][$id])) {
702
                    continue;
703
                }
704
705
                $theIdsInArray = GeneralUtility::trimExplode(',', $uidList, true);
706
                foreach ($theIdsInArray as $copyToUid) {
707
                    $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
708
                }
709
            }
710
        }
711
    }
712
713
    /**
714
     * Initializes default values coming from User TSconfig
715
     *
716
     * @param array $userTS User TSconfig array
717
     * @internal should only be used from within DataHandler
718
     */
719
    public function setDefaultsFromUserTS($userTS)
720
    {
721
        if (!is_array($userTS)) {
0 ignored issues
show
introduced by
The condition is_array($userTS) is always true.
Loading history...
722
            return;
723
        }
724
725
        foreach ($userTS as $k => $v) {
726
            $k = mb_substr($k, 0, -1);
727
            if (!$k || !is_array($v) || !isset($GLOBALS['TCA'][$k])) {
728
                continue;
729
            }
730
731
            if (is_array($this->defaultValues[$k])) {
732
                $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
733
            } else {
734
                $this->defaultValues[$k] = $v;
735
            }
736
        }
737
    }
738
739
    /**
740
     * When a new record is created, all values that haven't been set but are set via PageTSconfig / UserTSconfig
741
     * get applied here.
742
     *
743
     * This is only executed for new records. The most important part is that the pageTS of the actual resolved $pid
744
     * is taken, and a new field array with empty defaults is set again.
745
     *
746
     * @param string $table
747
     * @param int $pageId
748
     * @param array $prepopulatedFieldArray
749
     * @return array
750
     */
751
    protected function applyDefaultsForFieldArray(string $table, int $pageId, array $prepopulatedFieldArray): array
752
    {
753
        // First set TCAdefaults respecting the given PageID
754
        $tcaDefaults = BackendUtility::getPagesTSconfig($pageId)['TCAdefaults.'] ?? null;
755
        // Re-apply $this->defaultValues settings
756
        $this->setDefaultsFromUserTS($tcaDefaults);
757
        $cleanFieldArray = $this->newFieldArray($table);
758
        if (isset($prepopulatedFieldArray['pid'])) {
759
            $cleanFieldArray['pid'] = $prepopulatedFieldArray['pid'];
760
        }
761
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? null;
762
        if ($sortColumn !== null && isset($prepopulatedFieldArray[$sortColumn])) {
763
            $cleanFieldArray[$sortColumn] = $prepopulatedFieldArray[$sortColumn];
764
        }
765
        return $cleanFieldArray;
766
    }
767
768
    /*********************************************
769
     *
770
     * HOOKS
771
     *
772
     *********************************************/
773
    /**
774
     * Hook: processDatamap_afterDatabaseOperations
775
     * (calls $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);)
776
     *
777
     * Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
778
     * but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
779
     *
780
     * @param array $hookObjectsArr (reference) Array with hook objects
781
     * @param string $status (reference) Status of the current operation, 'new' or 'update
782
     * @param string $table (reference) The table currently processing data for
783
     * @param string $id (reference) The record uid currently processing data for, [integer] or [string] (like 'NEW...')
784
     * @param array $fieldArray (reference) The field array of a record
785
     * @internal should only be used from within DataHandler
786
     */
787
    public function hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
788
    {
789
        // Process hook directly:
790
        if (!isset($this->remapStackRecords[$table][$id])) {
791
            foreach ($hookObjectsArr as $hookObj) {
792
                if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
793
                    $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
794
                }
795
            }
796
        } else {
797
            $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = [
798
                'status' => $status,
799
                'fieldArray' => $fieldArray,
800
                'hookObjectsArr' => $hookObjectsArr
801
            ];
802
        }
803
    }
804
805
    /**
806
     * Gets the 'checkModifyAccessList' hook objects.
807
     * The first call initializes the accordant objects.
808
     *
809
     * @return array The 'checkModifyAccessList' hook objects (if any)
810
     * @throws \UnexpectedValueException
811
     */
812
    protected function getCheckModifyAccessListHookObjects()
813
    {
814
        if (!isset($this->checkModifyAccessListHookObjects)) {
815
            $this->checkModifyAccessListHookObjects = [];
816
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] ?? [] as $className) {
817
                $hookObject = GeneralUtility::makeInstance($className);
818
                if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
819
                    throw new \UnexpectedValueException($className . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
820
                }
821
                $this->checkModifyAccessListHookObjects[] = $hookObject;
822
            }
823
        }
824
        return $this->checkModifyAccessListHookObjects;
825
    }
826
827
    /*********************************************
828
     *
829
     * PROCESSING DATA
830
     *
831
     *********************************************/
832
    /**
833
     * Processing the data-array
834
     * Call this function to process the data-array set by start()
835
     *
836
     * @return bool|void
837
     */
838
    public function process_datamap()
839
    {
840
        $this->controlActiveElements();
841
842
        // Keep versionized(!) relations here locally:
843
        $registerDBList = [];
844
        $this->registerElementsToBeDeleted();
845
        $this->datamap = $this->unsetElementsToBeDeleted($this->datamap);
846
        // Editing frozen:
847
        if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
848
            $this->newlog('All editing in this workspace has been frozen!', SystemLogErrorClassification::USER_ERROR);
849
            return false;
850
        }
851
        // First prepare user defined objects (if any) for hooks which extend this function:
852
        $hookObjectsArr = [];
853
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] ?? [] as $className) {
854
            $hookObject = GeneralUtility::makeInstance($className);
855
            if (method_exists($hookObject, 'processDatamap_beforeStart')) {
856
                $hookObject->processDatamap_beforeStart($this);
857
            }
858
            $hookObjectsArr[] = $hookObject;
859
        }
860
        // Pre-process data-map and synchronize localization states
861
        $this->datamap = GeneralUtility::makeInstance(SlugEnricher::class)->enrichDataMap($this->datamap);
862
        $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER, $this->referenceIndexUpdater)->process();
863
        // Organize tables so that the pages-table is always processed first. This is required if you want to make sure that content pointing to a new page will be created.
864
        $orderOfTables = [];
865
        // Set pages first.
866
        if (isset($this->datamap['pages'])) {
867
            $orderOfTables[] = 'pages';
868
        }
869
        $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
870
        // Process the tables...
871
        foreach ($orderOfTables as $table) {
872
            // Check if
873
            //	   - table is set in $GLOBALS['TCA'],
874
            //	   - table is NOT readOnly
875
            //	   - the table is set with content in the data-array (if not, there's nothing to process...)
876
            //	   - permissions for tableaccess OK
877
            $modifyAccessList = $this->checkModifyAccessList($table);
878
            if (!$modifyAccessList) {
879
                $this->log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
880
            }
881
            if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
882
                continue;
883
            }
884
885
            if ($this->reverseOrder) {
886
                $this->datamap[$table] = array_reverse($this->datamap[$table], 1);
887
            }
888
            // For each record from the table, do:
889
            // $id is the record uid, may be a string if new records...
890
            // $incomingFieldArray is the array of fields
891
            foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
892
                if (!is_array($incomingFieldArray)) {
893
                    continue;
894
                }
895
                $theRealPid = null;
896
897
                // Hook: processDatamap_preProcessFieldArray
898
                foreach ($hookObjectsArr as $hookObj) {
899
                    if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
900
                        $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
901
                    }
902
                }
903
                // ******************************
904
                // Checking access to the record
905
                // ******************************
906
                $createNewVersion = false;
907
                $recordAccess = false;
908
                $old_pid_value = '';
909
                // Is it a new record? (Then Id is a string)
910
                if (!MathUtility::canBeInterpretedAsInteger($id)) {
911
                    // Get a fieldArray with tca default values
912
                    $fieldArray = $this->newFieldArray($table);
913
                    // A pid must be set for new records.
914
                    if (isset($incomingFieldArray['pid'])) {
915
                        $pid_value = $incomingFieldArray['pid'];
916
                        // Checking and finding numerical pid, it may be a string-reference to another value
917
                        $canProceed = true;
918
                        // If a NEW... id
919
                        if (strpos($pid_value, 'NEW') !== false) {
920
                            if ($pid_value[0] === '-') {
921
                                $negFlag = -1;
922
                                $pid_value = substr($pid_value, 1);
923
                            } else {
924
                                $negFlag = 1;
925
                            }
926
                            // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
927
                            if (isset($this->substNEWwithIDs[$pid_value])) {
928
                                if ($negFlag === 1) {
929
                                    $old_pid_value = $this->substNEWwithIDs[$pid_value];
930
                                }
931
                                $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
932
                            } else {
933
                                $canProceed = false;
934
                            }
935
                        }
936
                        $pid_value = (int)$pid_value;
937
                        if ($canProceed) {
938
                            $fieldArray = $this->resolveSortingAndPidForNewRecord($table, $pid_value, $fieldArray);
939
                        }
940
                    }
941
                    $theRealPid = $fieldArray['pid'];
942
                    // Checks if records can be inserted on this $pid.
943
                    // If this is a page translation, the check needs to be done for the l10n_parent record
944
                    if ($table === 'pages' && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0 && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0) {
945
                        $recordAccess = $this->checkRecordInsertAccess($table, $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]);
946
                    } else {
947
                        $recordAccess = $this->checkRecordInsertAccess($table, $theRealPid);
948
                    }
949
                    if ($recordAccess) {
950
                        $this->addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray);
951
                        $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
952
                        if (!$recordAccess) {
953
                            $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
954
                        } elseif (!$this->bypassWorkspaceRestrictions && !$this->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
955
                            // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
956
                            // So, if no live records were allowed in the current workspace, we have to create a new version of this record
957
                            if (BackendUtility::isTableWorkspaceEnabled($table)) {
958
                                $createNewVersion = true;
959
                            } else {
960
                                $recordAccess = false;
961
                                $this->newlog('Record could not be created in this workspace', SystemLogErrorClassification::USER_ERROR);
962
                            }
963
                        }
964
                    }
965
                    // Yes new record, change $record_status to 'insert'
966
                    $status = 'new';
967
                } else {
968
                    // Nope... $id is a number
969
                    $fieldArray = [];
970
                    $recordAccess = $this->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $hookObjectsArr);
971
                    if (!$recordAccess) {
972
                        if ($this->enableLogging) {
973
                            $propArr = $this->getRecordProperties($table, $id);
974
                            $this->log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify record \'%s\' (%s) without permission. Or non-existing page.', 2, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
975
                        }
976
                        continue;
977
                    }
978
                    // Next check of the record permissions (internals)
979
                    $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
980
                    if (!$recordAccess) {
981
                        $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
982
                    } else {
983
                        // Here we fetch the PID of the record that we point to...
984
                        $tempdata = $this->recordInfo($table, $id, 'pid' . (BackendUtility::isTableWorkspaceEnabled($table) ? ',t3ver_oid,t3ver_wsid,t3ver_stage' : ''));
985
                        $theRealPid = $tempdata['pid'] ?? null;
986
                        // Use the new id of the versionized record we're trying to write to:
987
                        // (This record is a child record of a parent and has already been versionized.)
988
                        if (!empty($this->autoVersionIdMap[$table][$id])) {
989
                            // For the reason that creating a new version of this record, automatically
990
                            // created related child records (e.g. "IRRE"), update the accordant field:
991
                            $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
992
                            // Use the new id of the copied/versionized record:
993
                            $id = $this->autoVersionIdMap[$table][$id];
994
                            $recordAccess = true;
995
                        } elseif (!$this->bypassWorkspaceRestrictions && ($errorCode = $this->BE_USER->workspaceCannotEditRecord($table, $tempdata))) {
996
                            $recordAccess = false;
997
                            // Versioning is required and it must be offline version!
998
                            // Check if there already is a workspace version
999
                            $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
1000
                            if ($workspaceVersion) {
1001
                                $id = $workspaceVersion['uid'];
1002
                                $recordAccess = true;
1003
                            } elseif ($this->BE_USER->workspaceAllowAutoCreation($table, $id, $theRealPid)) {
1004
                                // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1005
                                $this->pagetreeNeedsRefresh = true;
1006
1007
                                /** @var DataHandler $tce */
1008
                                $tce = GeneralUtility::makeInstance(__CLASS__, $this->referenceIndexUpdater);
1009
                                $tce->enableLogging = $this->enableLogging;
1010
                                // Setting up command for creating a new version of the record:
1011
                                $cmd = [];
1012
                                $cmd[$table][$id]['version'] = [
1013
                                    'action' => 'new',
1014
                                    // Default is to create a version of the individual records
1015
                                    'label' => 'Auto-created for WS #' . $this->BE_USER->workspace
1016
                                ];
1017
                                $tce->start([], $cmd, $this->BE_USER);
1018
                                $tce->process_cmdmap();
1019
                                $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
1020
                                // If copying was successful, share the new uids (also of related children):
1021
                                if (!empty($tce->copyMappingArray[$table][$id])) {
1022
                                    foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
1023
                                        foreach ($origIdArray as $origId => $newId) {
1024
                                            $this->autoVersionIdMap[$origTable][$origId] = $newId;
1025
                                        }
1026
                                    }
1027
                                    // Update registerDBList, that holds the copied relations to child records:
1028
                                    $registerDBList = array_merge($registerDBList, $tce->registerDBList);
1029
                                    // For the reason that creating a new version of this record, automatically
1030
                                    // created related child records (e.g. "IRRE"), update the accordant field:
1031
                                    $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1032
                                    // Use the new id of the copied/versionized record:
1033
                                    $id = $this->autoVersionIdMap[$table][$id];
1034
                                    $recordAccess = true;
1035
                                } else {
1036
                                    $this->newlog('Could not be edited in offline workspace in the branch where found (failure state: \'' . $errorCode . '\'). Auto-creation of version failed!', SystemLogErrorClassification::USER_ERROR);
1037
                                }
1038
                            } else {
1039
                                $this->newlog('Could not be edited in offline workspace in the branch where found (failure state: \'' . $errorCode . '\'). Auto-creation of version not allowed in workspace!', SystemLogErrorClassification::USER_ERROR);
1040
                            }
1041
                        }
1042
                    }
1043
                    // The default is 'update'
1044
                    $status = 'update';
1045
                }
1046
                // If access was granted above, proceed to create or update record:
1047
                if (!$recordAccess) {
1048
                    continue;
1049
                }
1050
1051
                // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
1052
                [$tscPID] = BackendUtility::getTSCpid($table, $id, $old_pid_value ?: $fieldArray['pid']);
1053
                if ($status === 'new') {
1054
                    // Apply TCAdefaults from pageTS
1055
                    $fieldArray = $this->applyDefaultsForFieldArray($table, (int)$tscPID, $fieldArray);
1056
                    // Apply page permissions as well
1057
                    if ($table === 'pages') {
1058
                        $fieldArray = $this->pagePermissionAssembler->applyDefaults(
1059
                            $fieldArray,
1060
                            (int)$tscPID,
1061
                            (int)$this->userid,
1062
                            (int)$this->BE_USER->firstMainGroup
1063
                        );
1064
                    }
1065
                }
1066
                // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1067
                $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1068
                $newVersion_placeholderFieldArray = [];
1069
                if ($createNewVersion) {
1070
                    // create a placeholder array with already processed field content
1071
                    $newVersion_placeholderFieldArray = $fieldArray;
1072
                }
1073
                // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations to field!
1074
                // Forcing some values unto field array:
1075
                // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1076
                $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1077
                if ($createNewVersion) {
1078
                    $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1079
                }
1080
                // Setting system fields
1081
                if ($status === 'new') {
1082
                    if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1083
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1084
                        if ($createNewVersion) {
1085
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1086
                        }
1087
                    }
1088
                    if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1089
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1090
                        if ($createNewVersion) {
1091
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1092
                        }
1093
                    }
1094
                } elseif ($this->checkSimilar) {
1095
                    // Removing fields which are equal to the current value:
1096
                    $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1097
                }
1098
                if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1099
                    $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1100
                    if ($createNewVersion) {
1101
                        $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1102
                    }
1103
                }
1104
                // Set stage to "Editing" to make sure we restart the workflow
1105
                if (BackendUtility::isTableWorkspaceEnabled($table)) {
1106
                    $fieldArray['t3ver_stage'] = 0;
1107
                }
1108
                // Hook: processDatamap_postProcessFieldArray
1109
                foreach ($hookObjectsArr as $hookObj) {
1110
                    if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1111
                        $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1112
                    }
1113
                }
1114
                // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1115
                // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already
1116
                if (is_array($fieldArray)) {
1117
                    if ($status === 'new') {
1118
                        if ($table === 'pages') {
1119
                            // for new pages always a refresh is needed
1120
                            $this->pagetreeNeedsRefresh = true;
1121
                        }
1122
1123
                        // This creates a new version of the record with online placeholder and offline version
1124
                        if ($createNewVersion) {
1125
                            // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1126
                            $this->pagetreeNeedsRefresh = true;
1127
1128
                            // Setting placeholder state value for temporary record
1129
                            $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1130
                            // Setting workspace - only so display of placeholders can filter out those from other workspaces.
1131
                            $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1132
                            // Only set a label if it is an input field
1133
                            $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1134
                            if ($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['type'] === 'input') {
1135
                                $newVersion_placeholderFieldArray[$labelField] = $this->getPlaceholderTitleForTableLabel($table);
1136
                            }
1137
                            // Saving placeholder as 'original'
1138
                            $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1139
                            // For the actual new offline version, set versioning values to point to placeholder
1140
                            $fieldArray['pid'] = $theRealPid;
1141
                            $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1142
                            // Setting placeholder state value for version (so it can know it is currently a new version...)
1143
                            $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1144
                            $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1145
                            // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1146
                            $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1147
                            if ($phShadowId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $phShadowId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1148
                                // Processes fields of the placeholder record:
1149
                                $this->triggerRemapAction($table, $id, [$this, 'placeholderShadowing'], [$table, $phShadowId]);
1150
                                // Hold auto-versionized ids of placeholders:
1151
                                $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $phShadowId;
1152
                            }
1153
                        } else {
1154
                            $this->insertDB($table, $id, $fieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1155
                        }
1156
                    } else {
1157
                        if ($table === 'pages') {
1158
                            // only a certain number of fields needs to be checked for updates
1159
                            // if $this->checkSimilar is TRUE, fields with unchanged values are already removed here
1160
                            $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1161
                            if (!empty($fieldsToCheck)) {
1162
                                $this->pagetreeNeedsRefresh = true;
1163
                            }
1164
                        }
1165
                        $this->updateDB($table, $id, $fieldArray);
1166
                        $this->placeholderShadowing($table, $id);
1167
                    }
1168
                }
1169
                // Hook: processDatamap_afterDatabaseOperations
1170
                // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1171
                // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1172
                $this->hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1173
            }
1174
        }
1175
        // Process the stack of relations to remap/correct
1176
        $this->processRemapStack();
1177
        $this->dbAnalysisStoreExec();
1178
        // Hook: processDatamap_afterAllOperations
1179
        // Note: When this hook gets called, all operations on the submitted data have been finished.
1180
        foreach ($hookObjectsArr as $hookObj) {
1181
            if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1182
                $hookObj->processDatamap_afterAllOperations($this);
1183
            }
1184
        }
1185
1186
        if ($this->isOuterMostInstance()) {
1187
            $this->referenceIndexUpdater->update();
1188
            $this->processClearCacheQueue();
1189
            $this->resetElementsToBeDeleted();
1190
        }
1191
    }
1192
1193
    /**
1194
     * @param string $table
1195
     * @param string $value
1196
     * @param string $dbType
1197
     * @return string
1198
     */
1199
    protected function normalizeTimeFormat(string $table, string $value, string $dbType): string
1200
    {
1201
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1202
        $platform = $connection->getDatabasePlatform();
1203
        if ($platform instanceof SQLServerPlatform) {
1204
            $defaultLength = QueryHelper::getDateTimeFormats()[$dbType]['empty'];
1205
            $value = substr(
1206
                $value,
1207
                0,
1208
                strlen($defaultLength)
1209
            );
1210
        }
1211
        return $value;
1212
    }
1213
1214
    /**
1215
     * Sets the "sorting" DB field and the "pid" field of an incoming record that should be added (NEW1234)
1216
     * depending on the record that should be added or where it should be added.
1217
     *
1218
     * This method is called from process_datamap()
1219
     *
1220
     * @param string $table the table name of the record to insert
1221
     * @param int $pid the real PID (numeric) where the record should be
1222
     * @param array $fieldArray field+value pairs to add
1223
     * @return array the modified field array
1224
     */
1225
    protected function resolveSortingAndPidForNewRecord(string $table, int $pid, array $fieldArray): array
1226
    {
1227
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
1228
        // Points to a page on which to insert the element, possibly in the top of the page
1229
        if ($pid >= 0) {
1230
            // Ensure that the "pid" is not a translated page ID, but the default page ID
1231
            $pid = $this->getDefaultLanguagePageId($pid);
1232
            // The numerical pid is inserted in the data array
1233
            $fieldArray['pid'] = $pid;
1234
            // If this table is sorted we better find the top sorting number
1235
            if ($sortColumn) {
1236
                $fieldArray[$sortColumn] = $this->getSortNumber($table, 0, $pid);
1237
            }
1238
        } elseif ($sortColumn) {
1239
            // Points to another record before itself
1240
            // If this table is sorted we better find the top sorting number
1241
            // Because $pid is < 0, getSortNumber() returns an array
1242
            $sortingInfo = $this->getSortNumber($table, 0, $pid);
1243
            $fieldArray['pid'] = $sortingInfo['pid'];
1244
            $fieldArray[$sortColumn] = $sortingInfo['sortNumber'];
1245
        } else {
1246
            // Here we fetch the PID of the record that we point to
1247
            $record = $this->recordInfo($table, abs($pid), 'pid');
0 ignored issues
show
Bug introduced by
It seems like abs($pid) can also be of type double; however, parameter $id of TYPO3\CMS\Core\DataHandl...taHandler::recordInfo() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1247
            $record = $this->recordInfo($table, /** @scrutinizer ignore-type */ abs($pid), 'pid');
Loading history...
1248
            // Ensure that the "pid" is not a translated page ID, but the default page ID
1249
            $fieldArray['pid'] = $this->getDefaultLanguagePageId($record['pid']);
1250
        }
1251
        return $fieldArray;
1252
    }
1253
1254
    /**
1255
     * Fix shadowing of data in case we are editing an offline version of a live "New" placeholder record.
1256
     *
1257
     * @param string $table Table name
1258
     * @param int $id Record uid
1259
     * @internal should only be used from within DataHandler
1260
     */
1261
    public function placeholderShadowing($table, $id)
1262
    {
1263
        $liveRecord = BackendUtility::getLiveVersionOfRecord($table, $id, '*');
1264
        if (empty($liveRecord)) {
1265
            return;
1266
        }
1267
1268
        $liveState = VersionState::cast($liveRecord['t3ver_state']);
1269
        $versionRecord = BackendUtility::getRecord($table, $id);
1270
        $versionState = VersionState::cast($versionRecord['t3ver_state']);
1271
1272
        if (!$liveState->indicatesPlaceholder() || $versionState->equals(VersionState::MOVE_POINTER)) {
1273
            return;
1274
        }
1275
1276
        $placeholderRecord = $liveRecord;
1277
        $factory = GeneralUtility::makeInstance(
1278
            PlaceholderShadowColumnsResolver::class,
1279
            $table,
1280
            $GLOBALS['TCA'][$table] ?? []
1281
        );
1282
        $shadowColumns = $factory->forNewPlaceholder();
1283
        if (empty($shadowColumns)) {
1284
            return;
1285
        }
1286
1287
        $placeholderValues = [];
1288
        foreach ($shadowColumns as $fieldName) {
1289
            if ((string)$versionRecord[$fieldName] !== (string)$placeholderRecord[$fieldName]) {
1290
                $placeholderValues[$fieldName] = $versionRecord[$fieldName];
1291
            }
1292
        }
1293
        if (empty($placeholderValues)) {
1294
            return;
1295
        }
1296
1297
        if ($this->enableLogging) {
1298
            $this->log($table, $placeholderRecord['uid'], SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Shadowing done on fields <i>' . implode(',', array_keys($placeholderValues)) . '</i> in placeholder record ' . $table . ':' . $liveRecord['uid'] . ' (offline version UID=' . $id . ')', -1, [], $this->eventPid($table, $liveRecord['uid'], $liveRecord['pid']));
1299
        }
1300
        $this->updateDB($table, $placeholderRecord['uid'], $placeholderValues);
1301
    }
1302
1303
    /**
1304
     * Create a placeholder title for the label field that does match the field requirements
1305
     *
1306
     * @param string $table The table name
1307
     * @param string $placeholderContent Placeholder content to be used
1308
     * @return string placeholder value
1309
     * @internal should only be used from within DataHandler
1310
     */
1311
    public function getPlaceholderTitleForTableLabel($table, $placeholderContent = null)
1312
    {
1313
        if ($placeholderContent === null) {
1314
            $placeholderContent = 'PLACEHOLDER';
1315
        }
1316
1317
        $labelPlaceholder = '[' . $placeholderContent . ', WS#' . $this->BE_USER->workspace . ']';
1318
        $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1319
        if (!isset($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'])) {
1320
            return $labelPlaceholder;
1321
        }
1322
        $evalCodesArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'], true);
1323
        $transformedLabel = $this->checkValue_input_Eval($labelPlaceholder, $evalCodesArray, '', $table);
1324
        return $transformedLabel['value'] ?? $labelPlaceholder;
1325
    }
1326
1327
    /**
1328
     * Filling in the field array
1329
     * $this->excludedTablesAndFields is used to filter fields if needed.
1330
     *
1331
     * @param string $table Table name
1332
     * @param int $id Record ID
1333
     * @param array $fieldArray Default values, Preset $fieldArray with 'pid' maybe (pid and uid will be not be overridden anyway)
1334
     * @param array $incomingFieldArray Is which fields/values you want to set. There are processed and put into $fieldArray if OK
1335
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1336
     * @param string $status Is 'new' or 'update'
1337
     * @param int $tscPID TSconfig PID
1338
     * @return array Field Array
1339
     * @internal should only be used from within DataHandler
1340
     */
1341
    public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID)
1342
    {
1343
        // Initialize:
1344
        $originalLanguageRecord = null;
1345
        $originalLanguage_diffStorage = null;
1346
        $diffStorageFlag = false;
1347
        // Setting 'currentRecord' and 'checkValueRecord':
1348
        if (strpos($id, 'NEW') !== false) {
1349
            // Must have the 'current' array - not the values after processing below...
1350
            $checkValueRecord = $fieldArray;
1351
            // IF $incomingFieldArray is an array, overlay it.
1352
            // The point is that when new records are created as copies with flex type fields there might be a field containing information about which DataStructure to use and without that information the flexforms cannot be correctly processed.... This should be OK since the $checkValueRecord is used by the flexform evaluation only anyways...
1353
            if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {
0 ignored issues
show
introduced by
The condition is_array($checkValueRecord) is always true.
Loading history...
1354
                ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);
1355
            }
1356
            $currentRecord = $checkValueRecord;
1357
        } else {
1358
            // We must use the current values as basis for this!
1359
            $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));
1360
            // This is done to make the pid positive for offline versions; Necessary to have diff-view for page translations in workspaces.
1361
            BackendUtility::fixVersioningPid($table, $currentRecord);
1362
        }
1363
1364
        // Get original language record if available:
1365
        if (is_array($currentRecord)
1366
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']
1367
            && $GLOBALS['TCA'][$table]['ctrl']['languageField']
1368
            && $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
1369
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
1370
            && (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0
1371
        ) {
1372
            $originalLanguageRecord = $this->recordInfo($table, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');
1373
            BackendUtility::workspaceOL($table, $originalLanguageRecord);
1374
            $originalLanguage_diffStorage = json_decode(
1375
                (string)($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] ?? ''),
1376
                true
1377
            );
1378
        }
1379
1380
        $this->checkValue_currentRecord = $checkValueRecord;
1381
        // In the following all incoming value-fields are tested:
1382
        // - Are the user allowed to change the field?
1383
        // - Is the field uid/pid (which are already set)
1384
        // - perms-fields for pages-table, then do special things...
1385
        // - If the field is nothing of the above and the field is configured in TCA, the fieldvalues are evaluated by ->checkValue
1386
        // If everything is OK, the field is entered into $fieldArray[]
1387
        foreach ($incomingFieldArray as $field => $fieldValue) {
1388
            if (isset($this->excludedTablesAndFields[$table . '-' . $field]) || $this->data_disableFields[$table][$id][$field]) {
1389
                continue;
1390
            }
1391
1392
            // The field must be editable.
1393
            // Checking if a value for language can be changed:
1394
            $languageDeny = $GLOBALS['TCA'][$table]['ctrl']['languageField'] && (string)$GLOBALS['TCA'][$table]['ctrl']['languageField'] === (string)$field && !$this->BE_USER->checkLanguageAccess($fieldValue);
1395
            if ($languageDeny) {
1396
                continue;
1397
            }
1398
1399
            switch ($field) {
1400
                case 'uid':
1401
                case 'pid':
1402
                    // Nothing happens, already set
1403
                    break;
1404
                case 'perms_userid':
1405
                case 'perms_groupid':
1406
                case 'perms_user':
1407
                case 'perms_group':
1408
                case 'perms_everybody':
1409
                    // Permissions can be edited by the owner or the administrator
1410
                    if ($table === 'pages' && ($this->admin || $status === 'new' || $this->pageInfo($id, 'perms_userid') == $this->userid)) {
1411
                        $value = (int)$fieldValue;
1412
                        switch ($field) {
1413
                            case 'perms_userid':
1414
                            case 'perms_groupid':
1415
                                $fieldArray[$field] = $value;
1416
                                break;
1417
                            default:
1418
                                if ($value >= 0 && $value < (2 ** 5)) {
1419
                                    $fieldArray[$field] = $value;
1420
                                }
1421
                        }
1422
                    }
1423
                    break;
1424
                case 't3ver_oid':
1425
                case 't3ver_wsid':
1426
                case 't3ver_state':
1427
                case 't3ver_stage':
1428
                    break;
1429
                case 'l10n_state':
1430
                    $fieldArray[$field] = $fieldValue;
1431
                    break;
1432
                default:
1433
                    if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
1434
                        // Evaluating the value
1435
                        $res = $this->checkValue($table, $field, $fieldValue, $id, $status, $realPid, $tscPID, $incomingFieldArray);
1436
                        if (array_key_exists('value', $res)) {
1437
                            $fieldArray[$field] = $res['value'];
1438
                        }
1439
                        // Add the value of the original record to the diff-storage content:
1440
                        if ($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
1441
                            $originalLanguage_diffStorage[$field] = (string)$originalLanguageRecord[$field];
1442
                            $diffStorageFlag = true;
1443
                        }
1444
                    } elseif ($GLOBALS['TCA'][$table]['ctrl']['origUid'] === $field) {
1445
                        // Allow value for original UID to pass by...
1446
                        $fieldArray[$field] = $fieldValue;
1447
                    }
1448
            }
1449
        }
1450
1451
        // Dealing with a page translation, setting "sorting", "pid", "perms_*" to the same values as the original record
1452
        if ($table === 'pages' && is_array($originalLanguageRecord)) {
1453
            $fieldArray['sorting'] = $originalLanguageRecord['sorting'];
1454
            $fieldArray['perms_userid'] = $originalLanguageRecord['perms_userid'];
1455
            $fieldArray['perms_groupid'] = $originalLanguageRecord['perms_groupid'];
1456
            $fieldArray['perms_user'] = $originalLanguageRecord['perms_user'];
1457
            $fieldArray['perms_group'] = $originalLanguageRecord['perms_group'];
1458
            $fieldArray['perms_everybody'] = $originalLanguageRecord['perms_everybody'];
1459
        }
1460
1461
        // Add diff-storage information:
1462
        if ($diffStorageFlag
1463
            && !array_key_exists($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
1464
        ) {
1465
            // If the field is set it would probably be because of an undo-operation - in which case we should not update the field of course...
1466
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = json_encode($originalLanguage_diffStorage);
1467
        }
1468
        // Return fieldArray
1469
        return $fieldArray;
1470
    }
1471
1472
    /*********************************************
1473
     *
1474
     * Evaluation of input values
1475
     *
1476
     ********************************************/
1477
    /**
1478
     * Evaluates a value according to $table/$field settings.
1479
     * This function is for real database fields - NOT FlexForm "pseudo" fields.
1480
     * NOTICE: Calling this function expects this: 1) That the data is saved!
1481
     *
1482
     * @param string $table Table name
1483
     * @param string $field Field name
1484
     * @param string $value Value to be evaluated. Notice, this is the INPUT value from the form. The original value (from any existing record) must be manually looked up inside the function if needed - or taken from $currentRecord array.
1485
     * @param string $id The record-uid, mainly - but not exclusively - used for logging
1486
     * @param string $status 'update' or 'new' flag
1487
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1488
     * @param int $tscPID TSconfig PID
1489
     * @param array $incomingFieldArray the fields being explicitly set by the outside (unlike $fieldArray)
1490
     * @return array Returns the evaluated $value as key "value" in this array. Can be checked with isset($res['value']) ...
1491
     * @internal should only be used from within DataHandler
1492
     */
1493
    public function checkValue($table, $field, $value, $id, $status, $realPid, $tscPID, $incomingFieldArray = [])
1494
    {
1495
        $curValueRec = null;
1496
        // Result array
1497
        $res = [];
1498
1499
        // Processing special case of field pages.doktype
1500
        if ($table === 'pages' && $field === 'doktype') {
1501
            // If the user may not use this specific doktype, we issue a warning
1502
            if (!($this->admin || GeneralUtility::inList($this->BE_USER->groupData['pagetypes_select'], $value))) {
1503
                if ($this->enableLogging) {
1504
                    $propArr = $this->getRecordProperties($table, $id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Core\DataHandl...::getRecordProperties(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1504
                    $propArr = $this->getRecordProperties($table, /** @scrutinizer ignore-type */ $id);
Loading history...
1505
                    $this->log($table, $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'You cannot change the \'doktype\' of page \'%s\' to the desired value.', 1, [$propArr['header']], $propArr['event_pid']);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $recuid of TYPO3\CMS\Core\DataHandling\DataHandler::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1505
                    $this->log($table, /** @scrutinizer ignore-type */ $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'You cannot change the \'doktype\' of page \'%s\' to the desired value.', 1, [$propArr['header']], $propArr['event_pid']);
Loading history...
1506
                }
1507
                return $res;
1508
            }
1509
            if ($status === 'update') {
1510
                // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1511
                $onlyAllowedTables = $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1512
                if ($onlyAllowedTables) {
1513
                    // use the real page id (default language)
1514
                    $recordId = $this->getDefaultLanguagePageId($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $pageId of TYPO3\CMS\Core\DataHandl...DefaultLanguagePageId(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1514
                    $recordId = $this->getDefaultLanguagePageId(/** @scrutinizer ignore-type */ $id);
Loading history...
1515
                    $theWrongTables = $this->doesPageHaveUnallowedTables($recordId, $value);
0 ignored issues
show
Bug introduced by
$value of type string is incompatible with the type integer expected by parameter $doktype of TYPO3\CMS\Core\DataHandl...geHaveUnallowedTables(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1515
                    $theWrongTables = $this->doesPageHaveUnallowedTables($recordId, /** @scrutinizer ignore-type */ $value);
Loading history...
1516
                    if ($theWrongTables) {
1517
                        if ($this->enableLogging) {
1518
                            $propArr = $this->getRecordProperties($table, $id);
1519
                            $this->log($table, $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, '\'doktype\' of page \'%s\' could not be changed because the page contains records from disallowed tables; %s', 2, [$propArr['header'], $theWrongTables], $propArr['event_pid']);
1520
                        }
1521
                        return $res;
1522
                    }
1523
                }
1524
            }
1525
        }
1526
1527
        $curValue = null;
1528
        if ((int)$id !== 0) {
1529
            // Get current value:
1530
            $curValueRec = $this->recordInfo($table, $id, $field);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Core\DataHandl...taHandler::recordInfo(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1530
            $curValueRec = $this->recordInfo($table, /** @scrutinizer ignore-type */ $id, $field);
Loading history...
1531
            // isset() won't work here, since values can be NULL
1532
            if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1533
                $curValue = $curValueRec[$field];
1534
            }
1535
        }
1536
1537
        if ($table === 'be_users'
1538
            && ($field === 'admin' || $field === 'password')
1539
            && $status === 'update'
1540
        ) {
1541
            // Do not allow a non system maintainer admin to change admin flag and password of system maintainers
1542
            $systemMaintainers = array_map('intval', $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
1543
            // False if current user is not in system maintainer list or if switch to user mode is active
1544
            $isCurrentUserSystemMaintainer = $this->BE_USER->isSystemMaintainer();
1545
            $isTargetUserInSystemMaintainerList = in_array((int)$id, $systemMaintainers, true);
1546
            if ($field === 'admin') {
1547
                $isFieldChanged = (int)$curValueRec[$field] !== (int)$value;
1548
            } else {
1549
                $isFieldChanged = $curValueRec[$field] !== $value;
1550
            }
1551
            if (!$isCurrentUserSystemMaintainer && $isTargetUserInSystemMaintainerList && $isFieldChanged) {
1552
                $value = $curValueRec[$field];
1553
                $message = GeneralUtility::makeInstance(
1554
                    FlashMessage::class,
1555
                    $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.adminCanNotChangeSystemMaintainer'),
1556
                    '',
1557
                    FlashMessage::ERROR,
1558
                    true
1559
                );
1560
                $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1561
                $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
1562
            }
1563
        }
1564
1565
        // Getting config for the field
1566
        $tcaFieldConf = $this->resolveFieldConfigurationAndRespectColumnsOverrides($table, $field);
1567
1568
        // Create $recFID only for those types that need it
1569
        if ($tcaFieldConf['type'] === 'flex') {
1570
            $recFID = $table . ':' . $id . ':' . $field;
1571
        } else {
1572
            $recFID = null;
1573
        }
1574
1575
        // Perform processing:
1576
        $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, [], $tscPID, ['incomingFieldArray' => $incomingFieldArray]);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Core\DataHandl...andler::checkValue_SW(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1576
        $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, /** @scrutinizer ignore-type */ $id, $curValue, $status, $realPid, $recFID, $field, [], $tscPID, ['incomingFieldArray' => $incomingFieldArray]);
Loading history...
1577
        return $res;
1578
    }
1579
1580
    /**
1581
     * Use columns overrides for evaluation.
1582
     *
1583
     * Fetch the TCA ["config"] part for a specific field, including the columnsOverrides value.
1584
     * Used for checkValue purposes currently (as it takes the checkValue_currentRecord value).
1585
     *
1586
     * @param string $table
1587
     * @param string $field
1588
     * @return array
1589
     */
1590
    protected function resolveFieldConfigurationAndRespectColumnsOverrides(string $table, string $field): array
1591
    {
1592
        $tcaFieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1593
        $recordType = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1594
        $columnsOverridesConfigOfField = $GLOBALS['TCA'][$table]['types'][$recordType]['columnsOverrides'][$field]['config'] ?? null;
1595
        if ($columnsOverridesConfigOfField) {
1596
            ArrayUtility::mergeRecursiveWithOverrule($tcaFieldConf, $columnsOverridesConfigOfField);
1597
        }
1598
        return $tcaFieldConf;
1599
    }
1600
1601
    /**
1602
     * Branches out evaluation of a field value based on its type as configured in $GLOBALS['TCA']
1603
     * Can be called for FlexForm pseudo fields as well, BUT must not have $field set if so.
1604
     *
1605
     * @param array $res The result array. The processed value (if any!) is set in the "value" key.
1606
     * @param string $value The value to set.
1607
     * @param array $tcaFieldConf Field configuration from $GLOBALS['TCA']
1608
     * @param string $table Table name
1609
     * @param int $id UID of record
1610
     * @param mixed $curValue Current value of the field
1611
     * @param string $status 'update' or 'new' flag
1612
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1613
     * @param string $recFID Field identifier [table:uid:field] for flexforms
1614
     * @param string $field Field name. Must NOT be set if the call is for a flexform field (since flexforms are not allowed within flexforms).
1615
     * @param array $uploadedFiles
1616
     * @param int $tscPID TSconfig PID
1617
     * @param array $additionalData Additional data to be forwarded to sub-processors
1618
     * @return array Returns the evaluated $value as key "value" in this array.
1619
     * @internal should only be used from within DataHandler
1620
     */
1621
    public function checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID, array $additionalData = null)
1622
    {
1623
        // Convert to NULL value if defined in TCA
1624
        if ($value === null && !empty($tcaFieldConf['eval']) && GeneralUtility::inList($tcaFieldConf['eval'], 'null')) {
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
1625
            $res = ['value' => null];
1626
            return $res;
1627
        }
1628
1629
        switch ($tcaFieldConf['type']) {
1630
            case 'text':
1631
                $res = $this->checkValueForText($value, $tcaFieldConf, $table, $id, $realPid, $field);
1632
                break;
1633
            case 'passthrough':
1634
            case 'imageManipulation':
1635
            case 'user':
1636
                $res['value'] = $value;
1637
                break;
1638
            case 'input':
1639
                $res = $this->checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field);
1640
                break;
1641
            case 'slug':
1642
                $res = $this->checkValueForSlug((string)$value, $tcaFieldConf, $table, $id, (int)$realPid, $field, $additionalData['incomingFieldArray'] ?? []);
1643
                break;
1644
            case 'check':
1645
                $res = $this->checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1646
                break;
1647
            case 'radio':
1648
                $res = $this->checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1649
                break;
1650
            case 'group':
1651
            case 'select':
1652
                $res = $this->checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field);
1653
                break;
1654
            case 'inline':
1655
                $res = $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
1656
                break;
1657
            case 'flex':
1658
                // FlexForms are only allowed for real fields.
1659
                if ($field) {
1660
                    $res = $this->checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field);
1661
                }
1662
                break;
1663
            default:
1664
                // Do nothing
1665
        }
1666
        $res = $this->checkValueForInternalReferences($res, $value, $tcaFieldConf, $table, $id, $field);
1667
        return $res;
1668
    }
1669
1670
    /**
1671
     * Checks values that are used for internal references. If the provided $value
1672
     * is a NEW-identifier, the direct processing is stopped. Instead, the value is
1673
     * forwarded to the remap-stack to be post-processed and resolved into a proper
1674
     * UID after all data has been resolved.
1675
     *
1676
     * This method considers TCA types that cannot handle and resolve these internal
1677
     * values directly, like 'passthrough', 'none' or 'user'. Values are only modified
1678
     * here if the $field is used as 'transOrigPointerField' or 'translationSource'.
1679
     *
1680
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
1681
     * @param string $value The value to set.
1682
     * @param array $tcaFieldConf Field configuration from TCA
1683
     * @param string $table Table name
1684
     * @param int $id UID of record
1685
     * @param string $field The field name
1686
     * @return array The result array. The processed value (if any!) is set in the "value" key.
1687
     */
1688
    protected function checkValueForInternalReferences(array $res, $value, $tcaFieldConf, $table, $id, $field)
1689
    {
1690
        $relevantFieldNames = [
1691
            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null,
1692
            $GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null,
1693
        ];
1694
1695
        if (
1696
            // in case field is empty
1697
            empty($field)
1698
            // in case the field is not relevant
1699
            || !in_array($field, $relevantFieldNames)
1700
            // in case the 'value' index has been unset already
1701
            || !array_key_exists('value', $res)
1702
            // in case it's not a NEW-identifier
1703
            || strpos($value, 'NEW') === false
1704
        ) {
1705
            return $res;
1706
        }
1707
1708
        $valueArray = [$value];
1709
        $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
1710
        $this->addNewValuesToRemapStackChildIds($valueArray);
1711
        $this->remapStack[] = [
1712
            'args' => [$valueArray, $tcaFieldConf, $id, $table, $field],
1713
            'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 3],
1714
            'field' => $field
1715
        ];
1716
        unset($res['value']);
1717
1718
        return $res;
1719
    }
1720
1721
    /**
1722
     * Evaluate "text" type values.
1723
     *
1724
     * @param string $value The value to set.
1725
     * @param array $tcaFieldConf Field configuration from TCA
1726
     * @param string $table Table name
1727
     * @param int $id UID of record
1728
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1729
     * @param string $field Field name
1730
     * @return array $res The result array. The processed value (if any!) is set in the "value" key.
1731
     */
1732
    protected function checkValueForText($value, $tcaFieldConf, $table, $id, $realPid, $field)
1733
    {
1734
        if (isset($tcaFieldConf['eval']) && $tcaFieldConf['eval'] !== '') {
1735
            $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1736
            $evalCodesArray = $this->runtimeCache->get($cacheId);
1737
            if (!is_array($evalCodesArray)) {
1738
                $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1739
                $this->runtimeCache->set($cacheId, $evalCodesArray);
1740
            }
1741
            $valueArray = $this->checkValue_text_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1742
        } else {
1743
            $valueArray = ['value' => $value];
1744
        }
1745
1746
        // Handle richtext transformations
1747
        if ($this->dontProcessTransformations) {
1748
            return $valueArray;
1749
        }
1750
        // Keep null as value
1751
        if ($value === null) {
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
1752
            return $valueArray;
1753
        }
1754
        if (isset($tcaFieldConf['enableRichtext']) && (bool)$tcaFieldConf['enableRichtext'] === true) {
1755
            $recordType = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1756
            $richtextConfigurationProvider = GeneralUtility::makeInstance(Richtext::class);
1757
            $richtextConfiguration = $richtextConfigurationProvider->getConfiguration($table, $field, $realPid, $recordType, $tcaFieldConf);
1758
            $rteParser = GeneralUtility::makeInstance(RteHtmlParser::class);
1759
            $valueArray['value'] = $rteParser->transformTextForPersistence((string)$value, $richtextConfiguration['proc.'] ?? []);
1760
        }
1761
1762
        return $valueArray;
1763
    }
1764
1765
    /**
1766
     * Evaluate "input" type values.
1767
     *
1768
     * @param string $value The value to set.
1769
     * @param array $tcaFieldConf Field configuration from TCA
1770
     * @param string $table Table name
1771
     * @param int $id UID of record
1772
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1773
     * @param string $field Field name
1774
     * @return array $res The result array. The processed value (if any!) is set in the "value" key.
1775
     */
1776
    protected function checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
1777
    {
1778
        // Handle native date/time fields
1779
        $isDateOrDateTimeField = false;
1780
        $format = '';
1781
        $emptyValue = '';
1782
        $dateTimeTypes = QueryHelper::getDateTimeTypes();
1783
        // normal integer "date" fields (timestamps) are handled in checkValue_input_Eval
1784
        if (isset($tcaFieldConf['dbType']) && in_array($tcaFieldConf['dbType'], $dateTimeTypes, true)) {
1785
            if (empty($value)) {
1786
                $value = null;
1787
            } else {
1788
                $isDateOrDateTimeField = true;
1789
                $dateTimeFormats = QueryHelper::getDateTimeFormats();
1790
                $format = $dateTimeFormats[$tcaFieldConf['dbType']]['format'];
1791
1792
                // Convert the date/time into a timestamp for the sake of the checks
1793
                $emptyValue = $dateTimeFormats[$tcaFieldConf['dbType']]['empty'];
1794
                // We store UTC timestamps in the database, which is what getTimestamp() returns.
1795
                $dateTime = new \DateTime($value);
1796
                $value = $value === $emptyValue ? null : $dateTime->getTimestamp();
1797
            }
1798
        }
1799
        // Secures the string-length to be less than max.
1800
        if (isset($tcaFieldConf['max']) && (int)$tcaFieldConf['max'] > 0) {
1801
            $value = mb_substr((string)$value, 0, (int)$tcaFieldConf['max'], 'utf-8');
1802
        }
1803
1804
        if (empty($tcaFieldConf['eval'])) {
1805
            $res = ['value' => $value];
1806
        } else {
1807
            // Process evaluation settings:
1808
            $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1809
            $evalCodesArray = $this->runtimeCache->get($cacheId);
1810
            if (!is_array($evalCodesArray)) {
1811
                $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1812
                $this->runtimeCache->set($cacheId, $evalCodesArray);
1813
            }
1814
1815
            $res = $this->checkValue_input_Eval($value, $evalCodesArray, $tcaFieldConf['is_in'] ?? '', $table);
1816
            if (isset($tcaFieldConf['dbType']) && isset($res['value']) && !$res['value']) {
1817
                // set the value to null if we have an empty value for a native field
1818
                $res['value'] = null;
1819
            }
1820
1821
            // Process UNIQUE settings:
1822
            // Field is NOT set for flexForms - which also means that uniqueInPid and unique is NOT available for flexForm fields! Also getUnique should not be done for versioning
1823
            if ($field && !empty($res['value'])) {
1824
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
1825
                    $res['value'] = $this->getUnique($table, $field, $res['value'], $id, $realPid);
1826
                }
1827
                if ($res['value'] && in_array('unique', $evalCodesArray, true)) {
1828
                    $res['value'] = $this->getUnique($table, $field, $res['value'], $id);
1829
                }
1830
            }
1831
        }
1832
1833
        // Checking range of value:
1834
        // @todo: The "checkbox" option was removed for type=input, this check could be probably relaxed?
1835
        if (
1836
            isset($tcaFieldConf['range']) && $tcaFieldConf['range']
1837
            && (!isset($tcaFieldConf['checkbox']) || $res['value'] != $tcaFieldConf['checkbox'])
1838
            && (!isset($tcaFieldConf['default']) || (int)$res['value'] !== (int)$tcaFieldConf['default'])
1839
        ) {
1840
            if (isset($tcaFieldConf['range']['upper']) && (int)$res['value'] > (int)$tcaFieldConf['range']['upper']) {
1841
                $res['value'] = (int)$tcaFieldConf['range']['upper'];
1842
            }
1843
            if (isset($tcaFieldConf['range']['lower']) && (int)$res['value'] < (int)$tcaFieldConf['range']['lower']) {
1844
                $res['value'] = (int)$tcaFieldConf['range']['lower'];
1845
            }
1846
        }
1847
1848
        // Handle native date/time fields
1849
        if ($isDateOrDateTimeField) {
1850
            // Convert the timestamp back to a date/time
1851
            $res['value'] = $res['value'] ? gmdate($format, $res['value']) : $emptyValue;
1852
        }
1853
        return $res;
1854
    }
1855
1856
    /**
1857
     * Evaluate "slug" type values.
1858
     *
1859
     * @param string $value The value to set.
1860
     * @param array $tcaFieldConf Field configuration from TCA
1861
     * @param string $table Table name
1862
     * @param int $id UID of record
1863
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1864
     * @param string $field Field name
1865
     * @param array $incomingFieldArray the fields being explicitly set by the outside (unlike $fieldArray) for the record
1866
     * @return array $res The result array. The processed value (if any!) is set in the "value" key.
1867
     * @see SlugEnricher
1868
     * @see SlugHelper
1869
     */
1870
    protected function checkValueForSlug(string $value, array $tcaFieldConf, string $table, $id, int $realPid, string $field, array $incomingFieldArray = []): array
1871
    {
1872
        $workspaceId = $this->BE_USER->workspace;
1873
        $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $tcaFieldConf, $workspaceId);
1874
        $fullRecord = array_replace_recursive($this->checkValue_currentRecord, $incomingFieldArray ?? []);
1875
        // Generate a value if there is none, otherwise ensure that all characters are cleaned up
1876
        if ($value === '') {
1877
            $value = $helper->generate($fullRecord, $realPid);
1878
        } else {
1879
            $value = $helper->sanitize($value);
1880
        }
1881
1882
        // Return directly in case no evaluations are defined
1883
        if (empty($tcaFieldConf['eval'])) {
1884
            return ['value' => $value];
1885
        }
1886
1887
        $state = RecordStateFactory::forName($table)
1888
            ->fromArray($fullRecord, $realPid, $id);
1889
        $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1890
        if (in_array('unique', $evalCodesArray, true)) {
1891
            $value = $helper->buildSlugForUniqueInTable($value, $state);
1892
        }
1893
        if (in_array('uniqueInSite', $evalCodesArray, true)) {
1894
            $value = $helper->buildSlugForUniqueInSite($value, $state);
1895
        }
1896
        if (in_array('uniqueInPid', $evalCodesArray, true)) {
1897
            $value = $helper->buildSlugForUniqueInPid($value, $state);
1898
        }
1899
1900
        return ['value' => $value];
1901
    }
1902
1903
    /**
1904
     * Evaluates 'check' type values.
1905
     *
1906
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
1907
     * @param string $value The value to set.
1908
     * @param array $tcaFieldConf Field configuration from TCA
1909
     * @param string $table Table name
1910
     * @param int $id UID of record
1911
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1912
     * @param string $field Field name
1913
     * @return array Modified $res array
1914
     */
1915
    protected function checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
1916
    {
1917
        $items = $tcaFieldConf['items'];
1918
        if (!empty($tcaFieldConf['itemsProcFunc'])) {
1919
            /** @var ItemProcessingService $processingService */
1920
            $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
1921
            $items = $processingService->getProcessingItems(
1922
                $table,
1923
                $realPid,
1924
                $field,
1925
                $this->checkValue_currentRecord,
1926
                $tcaFieldConf,
1927
                $tcaFieldConf['items']
1928
            );
1929
        }
1930
1931
        $itemC = 0;
1932
        if ($items !== null) {
1933
            $itemC = count($items);
1934
        }
1935
        if (!$itemC) {
1936
            $itemC = 1;
1937
        }
1938
        $maxV = (2 ** $itemC) - 1;
1939
        if ($value < 0) {
1940
            // @todo: throw LogicException here? Negative values for checkbox items do not make sense and indicate a coding error.
1941
            $value = 0;
1942
        }
1943
        if ($value > $maxV) {
1944
            // @todo: This case is pretty ugly: If there is an itemsProcFunc registered, and if it returns a dynamic,
1945
            // @todo: changing list of items, then it may happen that a value is transformed and vanished checkboxes
1946
            // @todo: are permanently removed from the value.
1947
            // @todo: Suggestion: Throw an exception instead? Maybe a specific, catchable exception that generates a
1948
            // @todo: error message to the user - dynamic item sets via itemProcFunc on check would be a bad idea anyway.
1949
            $value = $value & $maxV;
1950
        }
1951
        if ($field && $value > 0 && !empty($tcaFieldConf['eval'])) {
1952
            $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1953
            $otherRecordsWithSameValue = [];
1954
            $maxCheckedRecords = 0;
1955
            if (in_array('maximumRecordsCheckedInPid', $evalCodesArray, true)) {
1956
                $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value, $realPid);
1957
                $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsCheckedInPid'];
1958
            }
1959
            if (in_array('maximumRecordsChecked', $evalCodesArray, true)) {
1960
                $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value);
1961
                $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsChecked'];
1962
            }
1963
1964
            // there are more than enough records with value "1" in the DB
1965
            // if so, set this value to "0" again
1966
            if ($maxCheckedRecords && count($otherRecordsWithSameValue) >= $maxCheckedRecords) {
1967
                $value = 0;
1968
                $this->log($table, $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Could not activate checkbox for field "%s". A total of %s record(s) can have this checkbox activated. Uncheck other records first in order to activate the checkbox of this record.', -1, [$this->getLanguageService()->sL(BackendUtility::getItemLabel($table, $field)), $maxCheckedRecords]);
1969
            }
1970
        }
1971
        $res['value'] = $value;
1972
        return $res;
1973
    }
1974
1975
    /**
1976
     * Evaluates 'radio' type values.
1977
     *
1978
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
1979
     * @param string $value The value to set.
1980
     * @param array $tcaFieldConf Field configuration from TCA
1981
     * @param string $table The table of the record
1982
     * @param int $id The id of the record
1983
     * @param int $pid The pid of the record
1984
     * @param string $field The field to check
1985
     * @return array Modified $res array
1986
     */
1987
    protected function checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
1988
    {
1989
        if (is_array($tcaFieldConf['items'])) {
1990
            foreach ($tcaFieldConf['items'] as $set) {
1991
                if ((string)$set[1] === (string)$value) {
1992
                    $res['value'] = $value;
1993
                    break;
1994
                }
1995
            }
1996
        }
1997
1998
        // if no value was found and an itemsProcFunc is defined, check that for the value
1999
        if ($tcaFieldConf['itemsProcFunc'] && empty($res['value'])) {
2000
            $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
2001
            $processedItems = $processingService->getProcessingItems(
2002
                $table,
2003
                $pid,
2004
                $field,
2005
                $this->checkValue_currentRecord,
2006
                $tcaFieldConf,
2007
                $tcaFieldConf['items']
2008
            );
2009
2010
            foreach ($processedItems as $set) {
2011
                if ((string)$set[1] === (string)$value) {
2012
                    $res['value'] = $value;
2013
                    break;
2014
                }
2015
            }
2016
        }
2017
2018
        return $res;
2019
    }
2020
2021
    /**
2022
     * Evaluates 'group' or 'select' type values.
2023
     *
2024
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2025
     * @param string|array $value The value to set.
2026
     * @param array $tcaFieldConf Field configuration from TCA
2027
     * @param string $table Table name
2028
     * @param int $id UID of record
2029
     * @param mixed $curValue Current value of the field
2030
     * @param string $status 'update' or 'new' flag
2031
     * @param string $recFID Field identifier [table:uid:field] for flexforms
2032
     * @param array $uploadedFiles
2033
     * @param string $field Field name
2034
     * @return array Modified $res array
2035
     */
2036
    protected function checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field)
2037
    {
2038
        // Detecting if value sent is an array and if so, implode it around a comma:
2039
        if (is_array($value)) {
2040
            $value = implode(',', $value);
2041
        }
2042
        // This converts all occurrences of '&#123;' to the byte 123 in the string - this is needed in very rare cases where file names with special characters (e.g. ???, umlaut) gets sent to the server as HTML entities instead of bytes. The error is done only by MSIE, not Mozilla and Opera.
2043
        // Anyway, this should NOT disturb anything else:
2044
        $value = $this->convNumEntityToByteValue($value);
2045
        // When values are sent as group or select they come as comma-separated values which are exploded by this function:
2046
        $valueArray = $this->checkValue_group_select_explodeSelectGroupValue($value);
2047
        // If multiple is not set, remove duplicates:
2048
        if (!$tcaFieldConf['multiple']) {
2049
            $valueArray = array_unique($valueArray);
2050
        }
2051
        // If an exclusive key is found, discard all others:
2052
        if ($tcaFieldConf['type'] === 'select' && $tcaFieldConf['exclusiveKeys']) {
2053
            $exclusiveKeys = GeneralUtility::trimExplode(',', $tcaFieldConf['exclusiveKeys']);
2054
            foreach ($valueArray as $index => $key) {
2055
                if (in_array($key, $exclusiveKeys, true)) {
2056
                    $valueArray = [$index => $key];
2057
                    break;
2058
                }
2059
            }
2060
        }
2061
        // This could be a good spot for parsing the array through a validation-function which checks if the values are correct (except that database references are not in their final form - but that is the point, isn't it?)
2062
        // NOTE!!! Must check max-items of files before the later check because that check would just leave out file names if there are too many!!
2063
        $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
2064
        // Checking for select / authMode, removing elements from $valueArray if any of them is not allowed!
2065
        if ($tcaFieldConf['type'] === 'select' && $tcaFieldConf['authMode']) {
2066
            $preCount = count($valueArray);
2067
            foreach ($valueArray as $index => $key) {
2068
                if (!$this->BE_USER->checkAuthMode($table, $field, $key, $tcaFieldConf['authMode'])) {
2069
                    unset($valueArray[$index]);
2070
                }
2071
            }
2072
            // During the check it turns out that the value / all values were removed - we respond by simply returning an empty array so nothing is written to DB for this field.
2073
            if ($preCount && empty($valueArray)) {
2074
                return [];
2075
            }
2076
        }
2077
        // For select types which has a foreign table attached:
2078
        $unsetResult = false;
2079
        if (
2080
            $tcaFieldConf['type'] === 'group' && $tcaFieldConf['internal_type'] === 'db'
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($tcaFieldConf['type'] =...ecial'] === 'languages', Probably Intended Meaning: $tcaFieldConf['type'] ==...cial'] === 'languages')
Loading history...
2081
            || $tcaFieldConf['type'] === 'select' && ($tcaFieldConf['foreign_table'] || isset($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages')
2082
        ) {
2083
            // check, if there is a NEW... id in the value, that should be substituted later
2084
            if (strpos($value, 'NEW') !== false) {
2085
                $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2086
                $this->addNewValuesToRemapStackChildIds($valueArray);
2087
                $this->remapStack[] = [
2088
                    'func' => 'checkValue_group_select_processDBdata',
2089
                    'args' => [$valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field],
2090
                    'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 5],
2091
                    'field' => $field
2092
                ];
2093
                $unsetResult = true;
2094
            } else {
2095
                $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field);
2096
            }
2097
        }
2098
        if (!$unsetResult) {
2099
            $newVal = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
2100
            $res['value'] = $this->castReferenceValue(implode(',', $newVal), $tcaFieldConf);
2101
        } else {
2102
            unset($res['value']);
2103
        }
2104
        return $res;
2105
    }
2106
2107
    /**
2108
     * Applies the filter methods from a column's TCA configuration to a value array.
2109
     *
2110
     * @param array $tcaFieldConfiguration
2111
     * @param array $values
2112
     * @return array|mixed
2113
     * @throws \RuntimeException
2114
     */
2115
    protected function applyFiltersToValues(array $tcaFieldConfiguration, array $values)
2116
    {
2117
        if (empty($tcaFieldConfiguration['filter']) || !is_array($tcaFieldConfiguration['filter'])) {
2118
            return $values;
2119
        }
2120
        foreach ($tcaFieldConfiguration['filter'] as $filter) {
2121
            if (empty($filter['userFunc'])) {
2122
                continue;
2123
            }
2124
            $parameters = $filter['parameters'] ?: [];
2125
            $parameters['values'] = $values;
2126
            $parameters['tcaFieldConfig'] = $tcaFieldConfiguration;
2127
            $values = GeneralUtility::callUserFunction($filter['userFunc'], $parameters, $this);
2128
            if (!is_array($values)) {
2129
                throw new \RuntimeException('Failed calling filter userFunc.', 1336051942);
2130
            }
2131
        }
2132
        return $values;
2133
    }
2134
2135
    /**
2136
     * Evaluates 'flex' type values.
2137
     *
2138
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2139
     * @param string|array $value The value to set.
2140
     * @param array $tcaFieldConf Field configuration from TCA
2141
     * @param string $table Table name
2142
     * @param int $id UID of record
2143
     * @param mixed $curValue Current value of the field
2144
     * @param string $status 'update' or 'new' flag
2145
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
2146
     * @param string $recFID Field identifier [table:uid:field] for flexforms
2147
     * @param int $tscPID TSconfig PID
2148
     * @param array $uploadedFiles Uploaded files for the field
2149
     * @param string $field Field name
2150
     * @return array Modified $res array
2151
     */
2152
    protected function checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field)
2153
    {
2154
        if (is_array($value)) {
2155
            // This value is necessary for flex form processing to happen on flexform fields in page records when they are copied.
2156
            // Problem: when copying a page, flexform XML comes along in the array for the new record - but since $this->checkValue_currentRecord
2157
            // does not have a uid or pid for that sake, the FlexFormTools->getDataStructureIdentifier() function returns no good DS. For new
2158
            // records we do know the expected PID so therefore we send that with this special parameter. Only active when larger than zero.
2159
            $row = $this->checkValue_currentRecord;
2160
            if ($status === 'new') {
2161
                $row['pid'] = $realPid;
2162
            }
2163
2164
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
2165
2166
            // Get data structure. The methods may throw various exceptions, with some of them being
2167
            // ok in certain scenarios, for instance on new record rows. Those are ok to "eat" here
2168
            // and substitute with a dummy DS.
2169
            $dataStructureArray = ['sheets' => ['sDEF' => []]];
2170
            try {
2171
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
2172
                    ['config' => $tcaFieldConf],
2173
                    $table,
2174
                    $field,
2175
                    $row
2176
                );
2177
2178
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
2179
            } catch (InvalidParentRowException|InvalidParentRowLoopException|InvalidParentRowRootException|InvalidPointerFieldValueException|InvalidIdentifierException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2180
            }
2181
2182
            // Get current value array:
2183
            $currentValueArray = (string)$curValue !== '' ? GeneralUtility::xml2array($curValue) : [];
2184
            if (!is_array($currentValueArray)) {
2185
                $currentValueArray = [];
2186
            }
2187
            // Remove all old meta for languages...
2188
            // Evaluation of input values:
2189
            $value['data'] = $this->checkValue_flex_procInData($value['data'] ?? [], $currentValueArray['data'] ?? [], $uploadedFiles['data'] ?? [], $dataStructureArray, [$table, $id, $curValue, $status, $realPid, $recFID, $tscPID]);
2190
            // Create XML from input value:
2191
            $xmlValue = $this->checkValue_flexArray2Xml($value, true);
2192
2193
            // Here we convert the currently submitted values BACK to an array, then merge the two and then BACK to XML again. This is needed to ensure the charsets are the same
2194
            // (provided that the current value was already stored IN the charset that the new value is converted to).
2195
            $arrValue = GeneralUtility::xml2array($xmlValue);
2196
2197
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'] ?? [] as $className) {
2198
                $hookObject = GeneralUtility::makeInstance($className);
2199
                if (method_exists($hookObject, 'checkFlexFormValue_beforeMerge')) {
2200
                    $hookObject->checkFlexFormValue_beforeMerge($this, $currentValueArray, $arrValue);
2201
                }
2202
            }
2203
2204
            ArrayUtility::mergeRecursiveWithOverrule($currentValueArray, $arrValue);
2205
            $xmlValue = $this->checkValue_flexArray2Xml($currentValueArray, true);
2206
2207
            // Action commands (sorting order and removals of elements) for flexform sections,
2208
            // see FormEngine for the use of this GP parameter
2209
            $actionCMDs = GeneralUtility::_GP('_ACTION_FLEX_FORMdata');
2210
            if (is_array($actionCMDs[$table][$id][$field]['data'] ?? null)) {
2211
                $arrValue = GeneralUtility::xml2array($xmlValue);
2212
                $this->_ACTION_FLEX_FORMdata($arrValue['data'], $actionCMDs[$table][$id][$field]['data']);
2213
                $xmlValue = $this->checkValue_flexArray2Xml($arrValue, true);
2214
            }
2215
            // Create the value XML:
2216
            $res['value'] = '';
2217
            $res['value'] .= $xmlValue;
2218
        } else {
2219
            // Passthrough...:
2220
            $res['value'] = $value;
2221
        }
2222
2223
        return $res;
2224
    }
2225
2226
    /**
2227
     * Converts an array to FlexForm XML
2228
     *
2229
     * @param array $array Array with FlexForm data
2230
     * @param bool $addPrologue If set, the XML prologue is returned as well.
2231
     * @return string Input array converted to XML
2232
     * @internal should only be used from within DataHandler
2233
     */
2234
    public function checkValue_flexArray2Xml($array, $addPrologue = false)
2235
    {
2236
        /** @var FlexFormTools $flexObj */
2237
        $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
2238
        return $flexObj->flexArray2Xml($array, $addPrologue);
2239
    }
2240
2241
    /**
2242
     * Actions for flex form element (move, delete)
2243
     * allows to remove and move flexform sections
2244
     *
2245
     * @param array $valueArray by reference
2246
     * @param array $actionCMDs
2247
     */
2248
    protected function _ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
2249
    {
2250
        if (!is_array($valueArray) || !is_array($actionCMDs)) {
0 ignored issues
show
introduced by
The condition is_array($actionCMDs) is always true.
Loading history...
introduced by
The condition is_array($valueArray) is always true.
Loading history...
2251
            return;
2252
        }
2253
2254
        foreach ($actionCMDs as $key => $value) {
2255
            if ($key === '_ACTION') {
2256
                // First, check if there are "commands":
2257
                if (empty(array_filter($actionCMDs[$key]))) {
2258
                    continue;
2259
                }
2260
2261
                asort($actionCMDs[$key]);
2262
                $newValueArray = [];
2263
                foreach ($actionCMDs[$key] as $idx => $order) {
2264
                    // Just one reflection here: It is clear that when removing elements from a flexform, then we will get lost
2265
                    // files unless we act on this delete operation by traversing and deleting files that were referred to.
2266
                    if ($order !== 'DELETE') {
2267
                        $newValueArray[$idx] = $valueArray[$idx];
2268
                    }
2269
                    unset($valueArray[$idx]);
2270
                }
2271
                $valueArray += $newValueArray;
2272
            } elseif (is_array($actionCMDs[$key]) && isset($valueArray[$key])) {
2273
                $this->_ACTION_FLEX_FORMdata($valueArray[$key], $actionCMDs[$key]);
2274
            }
2275
        }
2276
    }
2277
2278
    /**
2279
     * Evaluates 'inline' type values.
2280
     * (partly copied from the select_group function on this issue)
2281
     *
2282
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2283
     * @param string $value The value to set.
2284
     * @param array $tcaFieldConf Field configuration from TCA
2285
     * @param array $PP Additional parameters in a numeric array: $table,$id,$curValue,$status,$realPid,$recFID
2286
     * @param string $field Field name
2287
     * @param array $additionalData Additional data to be forwarded to sub-processors
2288
     * @internal should only be used from within DataHandler
2289
     */
2290
    public function checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData = null)
2291
    {
2292
        [$table, $id, , $status] = $PP;
2293
        $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
2294
    }
2295
2296
    /**
2297
     * Evaluates 'inline' type values.
2298
     * (partly copied from the select_group function on this issue)
2299
     *
2300
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2301
     * @param string $value The value to set.
2302
     * @param array $tcaFieldConf Field configuration from TCA
2303
     * @param string $table Table name
2304
     * @param int $id UID of record
2305
     * @param string $status 'update' or 'new' flag
2306
     * @param string $field Field name
2307
     * @param array $additionalData Additional data to be forwarded to sub-processors
2308
     * @return array|bool Modified $res array
2309
     * @internal should only be used from within DataHandler
2310
     */
2311
    public function checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData = null)
2312
    {
2313
        if (!$tcaFieldConf['foreign_table']) {
2314
            // Fatal error, inline fields should always have a foreign_table defined
2315
            return false;
2316
        }
2317
        // When values are sent they come as comma-separated values which are exploded by this function:
2318
        $valueArray = GeneralUtility::trimExplode(',', $value);
2319
        // Remove duplicates: (should not be needed)
2320
        $valueArray = array_unique($valueArray);
2321
        // Example for received data:
2322
        // $value = 45,NEW4555fdf59d154,12,123
2323
        // We need to decide whether we use the stack or can save the relation directly.
2324
        if (!empty($value) && (strpos($value, 'NEW') !== false || !MathUtility::canBeInterpretedAsInteger($id))) {
2325
            $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2326
            $this->addNewValuesToRemapStackChildIds($valueArray);
2327
            $this->remapStack[] = [
2328
                'func' => 'checkValue_inline_processDBdata',
2329
                'args' => [$valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData],
2330
                'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4],
2331
                'additionalData' => $additionalData,
2332
                'field' => $field,
2333
            ];
2334
            unset($res['value']);
2335
        } elseif ($value || MathUtility::canBeInterpretedAsInteger($id)) {
2336
            $res['value'] = $this->checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData);
2337
        }
2338
        return $res;
2339
    }
2340
2341
    /**
2342
     * Checks if a fields has more items than defined via TCA in maxitems.
2343
     * If there are more items than allowed, the item list is truncated to the defined number.
2344
     *
2345
     * @param array $tcaFieldConf Field configuration from TCA
2346
     * @param array $valueArray Current value array of items
2347
     * @return array The truncated value array of items
2348
     * @internal should only be used from within DataHandler
2349
     */
2350
    public function checkValue_checkMax($tcaFieldConf, $valueArray)
2351
    {
2352
        // BTW, checking for min and max items here does NOT make any sense when MM is used because the above function
2353
        // calls will just return an array with a single item (the count) if MM is used... Why didn't I perform the check
2354
        // before? Probably because we could not evaluate the validity of record uids etc... Hmm...
2355
        // NOTE to the comment: It's not really possible to check for too few items, because you must then determine first,
2356
        // if the field is actual used regarding the CType.
2357
        $maxitems = isset($tcaFieldConf['maxitems']) ? (int)$tcaFieldConf['maxitems'] : 99999;
2358
        return array_slice($valueArray, 0, $maxitems);
2359
    }
2360
2361
    /*********************************************
2362
     *
2363
     * Helper functions for evaluation functions.
2364
     *
2365
     ********************************************/
2366
    /**
2367
     * Gets a unique value for $table/$id/$field based on $value
2368
     *
2369
     * @param string $table Table name
2370
     * @param string $field Field name for which $value must be unique
2371
     * @param string $value Value string.
2372
     * @param int $id UID to filter out in the lookup (the record itself...)
2373
     * @param int $newPid If set, the value will be unique for this PID
2374
     * @return string Modified value (if not-unique). Will be the value appended with a number (until 100, then the function just breaks).
2375
     * @todo: consider workspaces, especially when publishing a unique value which has a unique value already in live
2376
     * @internal should only be used from within DataHandler
2377
     */
2378
    public function getUnique($table, $field, $value, $id, $newPid = 0)
2379
    {
2380
        if (!is_array($GLOBALS['TCA'][$table]) || !is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
2381
            // Field is not configured in TCA
2382
            return $value;
2383
        }
2384
2385
        if ((string)$GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'] === 'exclude') {
2386
            $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
2387
            $l10nParent = (int)$this->checkValue_currentRecord[$transOrigPointerField];
2388
            if ($l10nParent > 0) {
2389
                // Current record is a translation and l10n_mode "exclude" just copies the value from source language
2390
                return $value;
2391
            }
2392
        }
2393
2394
        $newValue = $originalValue = $value;
2395
        $statement = $this->getUniqueCountStatement($newValue, $table, $field, (int)$id, (int)$newPid);
2396
        // For as long as records with the test-value existing, try again (with incremented numbers appended)
2397
        if ($statement->fetchColumn()) {
2398
            for ($counter = 0; $counter <= 100; $counter++) {
2399
                $newValue = $value . $counter;
2400
                $statement->bindValue(1, $newValue);
2401
                $statement->execute();
2402
                if (!$statement->fetchColumn()) {
2403
                    break;
2404
                }
2405
            }
2406
        }
2407
2408
        if ($originalValue !== $newValue) {
2409
            $this->log($table, $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::WARNING, 'The value of the field "%s" has been changed from "%s" to "%s" as it is required to be unique.', 1, [$field, $originalValue, $newValue], $newPid);
2410
        }
2411
2412
        return $newValue;
2413
    }
2414
2415
    /**
2416
     * Gets the count of records for a unique field
2417
     *
2418
     * @param string $value The string value which should be unique
2419
     * @param string $table Table name
2420
     * @param string $field Field name for which $value must be unique
2421
     * @param int $uid UID to filter out in the lookup (the record itself...)
2422
     * @param int $pid If set, the value will be unique for this PID
2423
     * @return \Doctrine\DBAL\Driver\Statement Return the prepared statement to check uniqueness
2424
     */
2425
    protected function getUniqueCountStatement(
2426
        string $value,
2427
        string $table,
2428
        string $field,
2429
        int $uid,
2430
        int $pid
2431
    ): Statement {
2432
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2433
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
2434
        $queryBuilder
2435
            ->count('uid')
2436
            ->from($table)
2437
            ->where(
2438
                $queryBuilder->expr()->eq($field, $queryBuilder->createPositionalParameter($value, \PDO::PARAM_STR)),
2439
                $queryBuilder->expr()->neq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT))
2440
            );
2441
        // ignore translations of current record if field is configured with l10n_mode = "exclude"
2442
        if (($GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'] ?? '') === 'exclude'
2443
            && ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '') !== ''
2444
            && ($GLOBALS['TCA'][$table]['columns'][$field]['languageField'] ?? '') !== '') {
2445
            $queryBuilder
2446
                ->andWhere(
2447
                    $queryBuilder->expr()->orX(
2448
                    // records without l10n_parent must be taken into account (in any language)
2449
                        $queryBuilder->expr()->eq(
2450
                            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2451
                            $queryBuilder->createPositionalParameter(0, \PDO::PARAM_INT)
2452
                        ),
2453
                        // translations of other records must be taken into account
2454
                        $queryBuilder->expr()->neq(
2455
                            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2456
                            $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)
2457
                        )
2458
                    )
2459
                );
2460
        }
2461
        if ($pid !== 0) {
2462
            $queryBuilder->andWhere(
2463
                $queryBuilder->expr()->eq('pid', $queryBuilder->createPositionalParameter($pid, \PDO::PARAM_INT))
2464
            );
2465
        } else {
2466
            // pid>=0 for versioning
2467
            $queryBuilder->andWhere(
2468
                $queryBuilder->expr()->gte('pid', $queryBuilder->createPositionalParameter(0, \PDO::PARAM_INT))
2469
            );
2470
        }
2471
        return $queryBuilder->execute();
2472
    }
2473
2474
    /**
2475
     * gets all records that have the same value in a field
2476
     * excluding the given uid
2477
     *
2478
     * @param string $tableName Table name
2479
     * @param int $uid UID to filter out in the lookup (the record itself...)
2480
     * @param string $fieldName Field name for which $value must be unique
2481
     * @param string $value Value string.
2482
     * @param int $pageId If set, the value will be unique for this PID
2483
     * @return array
2484
     * @internal should only be used from within DataHandler
2485
     */
2486
    public function getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId = 0)
2487
    {
2488
        $result = [];
2489
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
2490
            return $result;
2491
        }
2492
2493
        $uid = (int)$uid;
2494
        $pageId = (int)$pageId;
2495
2496
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
2497
        $queryBuilder->getRestrictions()
2498
            ->removeAll()
2499
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2500
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
2501
2502
        $queryBuilder->select('*')
2503
            ->from($tableName)
2504
            ->where(
2505
                $queryBuilder->expr()->eq(
2506
                    $fieldName,
2507
                    $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
2508
                ),
2509
                $queryBuilder->expr()->neq(
2510
                    'uid',
2511
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
2512
                )
2513
            );
2514
2515
        if ($pageId) {
2516
            $queryBuilder->andWhere(
2517
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT))
2518
            );
2519
        }
2520
2521
        $result = $queryBuilder->execute()->fetchAll();
2522
2523
        return $result;
2524
    }
2525
2526
    /**
2527
     * @param string $value The field value to be evaluated
2528
     * @param array $evalArray Array of evaluations to traverse.
2529
     * @param string $is_in The "is_in" value of the field configuration from TCA
2530
     * @return array
2531
     * @internal should only be used from within DataHandler
2532
     */
2533
    public function checkValue_text_Eval($value, $evalArray, $is_in)
2534
    {
2535
        $res = [];
2536
        $set = true;
2537
        foreach ($evalArray as $func) {
2538
            switch ($func) {
2539
                case 'trim':
2540
                    $value = trim($value);
2541
                    break;
2542
                case 'required':
2543
                    if (!$value) {
2544
                        $set = false;
2545
                    }
2546
                    break;
2547
                default:
2548
                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2549
                        if (class_exists($func)) {
2550
                            $evalObj = GeneralUtility::makeInstance($func);
2551
                            if (method_exists($evalObj, 'evaluateFieldValue')) {
2552
                                $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2553
                            }
2554
                        }
2555
                    }
2556
            }
2557
        }
2558
        if ($set) {
2559
            $res['value'] = $value;
2560
        }
2561
        return $res;
2562
    }
2563
2564
    /**
2565
     * Evaluation of 'input'-type values based on 'eval' list
2566
     *
2567
     * @param string $value Value to evaluate
2568
     * @param array $evalArray Array of evaluations to traverse.
2569
     * @param string $is_in Is-in string for 'is_in' evaluation
2570
     * @param string $table Table name the eval is evaluated on
2571
     * @return array Modified $value in key 'value' or empty array
2572
     * @internal should only be used from within DataHandler
2573
     */
2574
    public function checkValue_input_Eval($value, $evalArray, $is_in, string $table = ''): array
2575
    {
2576
        $res = [];
2577
        $set = true;
2578
        foreach ($evalArray as $func) {
2579
            switch ($func) {
2580
                case 'int':
2581
                case 'year':
2582
                    $value = (int)$value;
2583
                    break;
2584
                case 'time':
2585
                case 'timesec':
2586
                    // If $value is a pure integer we have the number of seconds, we can store that directly
2587
                    if ($value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
2588
                        // $value is an ISO 8601 date
2589
                        $value = (new \DateTime($value))->getTimestamp();
2590
                    }
2591
                    break;
2592
                case 'date':
2593
                case 'datetime':
2594
                    // If $value is a pure integer we have the number of seconds, we can store that directly
2595
                    if ($value !== null && $value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
2596
                        // The value we receive from JS is an ISO 8601 date, which is always in UTC. (the JS code works like that, on purpose!)
2597
                        // For instance "1999-11-11T11:11:11Z"
2598
                        // Since the user actually specifies the time in the server's local time, we need to mangle this
2599
                        // to reflect the server TZ. So we make this 1999-11-11T11:11:11+0200 (assuming Europe/Vienna here)
2600
                        // In the database we store the date in UTC (1999-11-11T09:11:11Z), hence we take the timestamp of this converted value.
2601
                        // For achieving this we work with timestamps only (which are UTC) and simply adjust it for the
2602
                        // TZ difference.
2603
                        try {
2604
                            // Make the date from JS a timestamp
2605
                            $value = (new \DateTime($value))->getTimestamp();
2606
                        } catch (\Exception $e) {
2607
                            // set the default timezone value to achieve the value of 0 as a result
2608
                            $value = (int)date('Z', 0);
2609
                        }
2610
2611
                        // @todo this hacky part is problematic when it comes to times around DST switch! Add test to prove that this is broken.
2612
                        $value -= date('Z', $value);
2613
                    }
2614
                    break;
2615
                case 'double2':
2616
                    $value = preg_replace('/[^0-9,\\.-]/', '', $value);
2617
                    $negative = $value[0] === '-';
2618
                    $value = strtr($value, [',' => '.', '-' => '']);
2619
                    if (strpos($value, '.') === false) {
2620
                        $value .= '.0';
2621
                    }
2622
                    $valueArray = explode('.', $value);
2623
                    $dec = array_pop($valueArray);
2624
                    $value = implode('', $valueArray) . '.' . $dec;
2625
                    if ($negative) {
2626
                        $value *= -1;
2627
                    }
2628
                    $value = number_format($value, 2, '.', '');
0 ignored issues
show
Bug introduced by
$value of type string is incompatible with the type double expected by parameter $number of number_format(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2628
                    $value = number_format(/** @scrutinizer ignore-type */ $value, 2, '.', '');
Loading history...
2629
                    break;
2630
                case 'md5':
2631
                    if (strlen($value) !== 32) {
2632
                        $set = false;
2633
                    }
2634
                    break;
2635
                case 'trim':
2636
                    $value = trim($value);
2637
                    break;
2638
                case 'upper':
2639
                    $value = mb_strtoupper($value, 'utf-8');
2640
                    break;
2641
                case 'lower':
2642
                    $value = mb_strtolower($value, 'utf-8');
2643
                    break;
2644
                case 'required':
2645
                    if (!isset($value) || $value === '') {
2646
                        $set = false;
2647
                    }
2648
                    break;
2649
                case 'is_in':
2650
                    $c = mb_strlen($value);
2651
                    if ($c) {
2652
                        $newVal = '';
2653
                        for ($a = 0; $a < $c; $a++) {
2654
                            $char = mb_substr($value, $a, 1);
2655
                            if (mb_strpos($is_in, $char) !== false) {
2656
                                $newVal .= $char;
2657
                            }
2658
                        }
2659
                        $value = $newVal;
2660
                    }
2661
                    break;
2662
                case 'nospace':
2663
                    $value = str_replace(' ', '', $value);
2664
                    break;
2665
                case 'alpha':
2666
                    $value = preg_replace('/[^a-zA-Z]/', '', $value);
2667
                    break;
2668
                case 'num':
2669
                    $value = preg_replace('/[^0-9]/', '', $value);
2670
                    break;
2671
                case 'alphanum':
2672
                    $value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
2673
                    break;
2674
                case 'alphanum_x':
2675
                    $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
2676
                    break;
2677
                case 'domainname':
2678
                    if (!preg_match('/^[a-z0-9.\\-]*$/i', $value)) {
2679
                        $value = (string)HttpUtility::idn_to_ascii($value);
2680
                    }
2681
                    break;
2682
                case 'email':
2683
                    if ((string)$value !== '') {
2684
                        $this->checkValue_input_ValidateEmail($value, $set);
2685
                    }
2686
                    break;
2687
                case 'saltedPassword':
2688
                    // An incoming value is either the salted password if the user did not change existing password
2689
                    // when submitting the form, or a plaintext new password that needs to be turned into a salted password now.
2690
                    // The strategy is to see if a salt instance can be created from the incoming value. If so,
2691
                    // no new password was submitted and we keep the value. If no salting instance can be created,
2692
                    // incoming value must be a new plain text value that needs to be hashed.
2693
                    $hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
2694
                    $mode = $table === 'fe_users' ? 'FE' : 'BE';
2695
                    try {
2696
                        $hashFactory->get($value, $mode);
2697
                    } catch (InvalidPasswordHashException $e) {
2698
                        // We got no salted password instance, incoming value must be a new plaintext password
2699
                        // Get an instance of the current configured salted password strategy and hash the value
2700
                        $newHashInstance = $hashFactory->getDefaultHashInstance($mode);
2701
                        $value = $newHashInstance->getHashedPassword($value);
2702
                    }
2703
                    break;
2704
                default:
2705
                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2706
                        if (class_exists($func)) {
2707
                            $evalObj = GeneralUtility::makeInstance($func);
2708
                            if (method_exists($evalObj, 'evaluateFieldValue')) {
2709
                                $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2710
                            }
2711
                        }
2712
                    }
2713
            }
2714
        }
2715
        if ($set) {
2716
            $res['value'] = $value;
2717
        }
2718
        return $res;
2719
    }
2720
2721
    /**
2722
     * If $value is not a valid e-mail address,
2723
     * $set will be set to false and a flash error
2724
     * message will be added
2725
     *
2726
     * @param string $value Value to evaluate
2727
     * @param bool $set TRUE if an update should be done
2728
     * @throws \InvalidArgumentException
2729
     * @throws \TYPO3\CMS\Core\Exception
2730
     */
2731
    protected function checkValue_input_ValidateEmail($value, &$set)
2732
    {
2733
        if (GeneralUtility::validEmail($value)) {
2734
            return;
2735
        }
2736
2737
        $set = false;
2738
        /** @var FlashMessage $message */
2739
        $message = GeneralUtility::makeInstance(
2740
            FlashMessage::class,
2741
            sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.invalidEmail'), $value),
2742
            '', // header is optional
2743
            FlashMessage::ERROR,
2744
            true // whether message should be stored in session
2745
        );
2746
        /** @var FlashMessageService $flashMessageService */
2747
        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
2748
        $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
2749
    }
2750
2751
    /**
2752
     * Returns data for group/db and select fields
2753
     *
2754
     * @param array $valueArray Current value array
2755
     * @param array $tcaFieldConf TCA field config
2756
     * @param int $id Record id, used for look-up of MM relations (local_uid)
2757
     * @param string $status Status string ('update' or 'new')
2758
     * @param string $type The type, either 'select', 'group' or 'inline'
2759
     * @param string $currentTable Table name, needs to be passed to \TYPO3\CMS\Core\Database\RelationHandler
2760
     * @param string $currentField field name, needs to be set for writing to sys_history
2761
     * @return array Modified value array
2762
     * @internal should only be used from within DataHandler
2763
     */
2764
    public function checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
2765
    {
2766
        if ($type === 'group') {
2767
            $tables = $tcaFieldConf['allowed'];
2768
        } elseif (!empty($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages') {
2769
            $tables = 'sys_language';
2770
        } else {
2771
            $tables = $tcaFieldConf['foreign_table'];
2772
        }
2773
        $prep = $type === 'group' ? $tcaFieldConf['prepend_tname'] : '';
2774
        $newRelations = implode(',', $valueArray);
2775
        /** @var RelationHandler $dbAnalysis */
2776
        $dbAnalysis = $this->createRelationHandlerInstance();
2777
        $dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2778
        $dbAnalysis->start($newRelations, $tables, '', 0, $currentTable, $tcaFieldConf);
2779
        if ($tcaFieldConf['MM']) {
2780
            // convert submitted items to use version ids instead of live ids
2781
            // (only required for MM relations in a workspace context)
2782
            $dbAnalysis->convertItemArray();
2783
            if ($status === 'update') {
2784
                /** @var RelationHandler $oldRelations_dbAnalysis */
2785
                $oldRelations_dbAnalysis = $this->createRelationHandlerInstance();
2786
                $oldRelations_dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2787
                // Db analysis with $id will initialize with the existing relations
2788
                $oldRelations_dbAnalysis->start('', $tables, $tcaFieldConf['MM'], $id, $currentTable, $tcaFieldConf);
2789
                $oldRelations = implode(',', $oldRelations_dbAnalysis->getValueArray());
2790
                $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, $prep);
0 ignored issues
show
Bug introduced by
It seems like $prep can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\RelationHandler::writeMM() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2790
                $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, /** @scrutinizer ignore-type */ $prep);
Loading history...
2791
                if ($oldRelations != $newRelations) {
2792
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
2793
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
2794
                } else {
2795
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
2796
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
2797
                }
2798
            } else {
2799
                $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
2800
            }
2801
            $valueArray = $dbAnalysis->countItems();
2802
        } else {
2803
            $valueArray = $dbAnalysis->getValueArray($prep);
0 ignored issues
show
Bug introduced by
It seems like $prep can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\...andler::getValueArray() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2803
            $valueArray = $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prep);
Loading history...
2804
        }
2805
        // Here we should see if 1) the records exist anymore, 2) which are new and check if the BE_USER has read-access to the new ones.
2806
        return $valueArray;
2807
    }
2808
2809
    /**
2810
     * Explodes the $value, which is a list of files/uids (group select)
2811
     *
2812
     * @param string $value Input string, comma separated values. For each part it will also be detected if a '|' is found and the first part will then be used if that is the case. Further the value will be rawurldecoded.
2813
     * @return array The value array.
2814
     * @internal should only be used from within DataHandler
2815
     */
2816
    public function checkValue_group_select_explodeSelectGroupValue($value)
2817
    {
2818
        $valueArray = GeneralUtility::trimExplode(',', $value, true);
2819
        foreach ($valueArray as &$newVal) {
2820
            $temp = explode('|', $newVal, 2);
2821
            $newVal = str_replace(['|', ','], '', rawurldecode($temp[0]));
2822
        }
2823
        unset($newVal);
2824
        return $valueArray;
2825
    }
2826
2827
    /**
2828
     * Starts the processing the input data for flexforms. This will traverse all sheets / languages and for each it will traverse the sub-structure.
2829
     * See checkValue_flex_procInData_travDS() for more details.
2830
     * WARNING: Currently, it traverses based on the actual _data_ array and NOT the _structure_. This means that values for non-valid fields, lKey/vKey/sKeys will be accepted! For traversal of data with a call back function you should rather use \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
2831
     *
2832
     * @param array $dataPart The 'data' part of the INPUT flexform data
2833
     * @param array $dataPart_current The 'data' part of the CURRENT flexform data
2834
     * @param array $uploadedFiles The uploaded files for the 'data' part of the INPUT flexform data
2835
     * @param array $dataStructure Data structure for the form (might be sheets or not). Only values in the data array which has a configuration in the data structure will be processed.
2836
     * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions
2837
     * @param string $callBackFunc Optional call back function, see checkValue_flex_procInData_travDS()  DEPRECATED, use \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools instead for traversal!
2838
     * @param array $workspaceOptions
2839
     * @return array The modified 'data' part.
2840
     * @see checkValue_flex_procInData_travDS()
2841
     * @internal should only be used from within DataHandler
2842
     */
2843
    public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructure, $pParams, $callBackFunc = '', array $workspaceOptions = [])
2844
    {
2845
        if (is_array($dataPart)) {
0 ignored issues
show
introduced by
The condition is_array($dataPart) is always true.
Loading history...
2846
            foreach ($dataPart as $sKey => $sheetDef) {
2847
                if (isset($dataStructure['sheets'][$sKey]) && is_array($dataStructure['sheets'][$sKey]) && is_array($sheetDef)) {
2848
                    foreach ($sheetDef as $lKey => $lData) {
2849
                        $this->checkValue_flex_procInData_travDS(
2850
                            $dataPart[$sKey][$lKey],
2851
                            $dataPart_current[$sKey][$lKey],
2852
                            $uploadedFiles[$sKey][$lKey],
2853
                            $dataStructure['sheets'][$sKey]['ROOT']['el'],
2854
                            $pParams,
2855
                            $callBackFunc,
2856
                            $sKey . '/' . $lKey . '/',
2857
                            $workspaceOptions
2858
                        );
2859
                    }
2860
                }
2861
            }
2862
        }
2863
        return $dataPart;
2864
    }
2865
2866
    /**
2867
     * Processing of the sheet/language data array
2868
     * When it finds a field with a value the processing is done by ->checkValue_SW() by default but if a call back function name is given that method in this class will be called for the processing instead.
2869
     *
2870
     * @param array $dataValues New values (those being processed): Multidimensional Data array for sheet/language, passed by reference!
2871
     * @param array $dataValues_current Current values: Multidimensional Data array. May be empty array() if not needed (for callBackFunctions)
2872
     * @param array $uploadedFiles Uploaded files array for sheet/language. May be empty array() if not needed (for callBackFunctions)
2873
     * @param array $DSelements Data structure which fits the data array
2874
     * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions / call back function
2875
     * @param string $callBackFunc Call back function, default is checkValue_SW(). If $this->callBackObj is set to an object, the callback function in that object is called instead.
2876
     * @param string $structurePath
2877
     * @param array $workspaceOptions
2878
     * @see checkValue_flex_procInData()
2879
     * @internal should only be used from within DataHandler
2880
     */
2881
    public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions = [])
2882
    {
2883
        if (!is_array($DSelements)) {
0 ignored issues
show
introduced by
The condition is_array($DSelements) is always true.
Loading history...
2884
            return;
2885
        }
2886
2887
        // For each DS element:
2888
        foreach ($DSelements as $key => $dsConf) {
2889
            // Array/Section:
2890
            if ($DSelements[$key]['type'] === 'array') {
2891
                if (!is_array($dataValues[$key]['el'])) {
2892
                    continue;
2893
                }
2894
2895
                if ($DSelements[$key]['section']) {
2896
                    foreach ($dataValues[$key]['el'] as $ik => $el) {
2897
                        if (!is_array($el)) {
2898
                            continue;
2899
                        }
2900
2901
                        if (!is_array($dataValues_current[$key]['el'])) {
2902
                            $dataValues_current[$key]['el'] = [];
2903
                        }
2904
                        $theKey = key($el);
2905
                        if (!is_array($dataValues[$key]['el'][$ik][$theKey]['el'])) {
2906
                            continue;
2907
                        }
2908
2909
                        $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'][$ik][$theKey]['el'], is_array($dataValues_current[$key]['el'][$ik]) ? $dataValues_current[$key]['el'][$ik][$theKey]['el'] : [], $uploadedFiles[$key]['el'][$ik][$theKey]['el'], $DSelements[$key]['el'][$theKey]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/' . $ik . '/' . $theKey . '/el/', $workspaceOptions);
2910
                    }
2911
                } else {
2912
                    if (!isset($dataValues[$key]['el'])) {
2913
                        $dataValues[$key]['el'] = [];
2914
                    }
2915
                    $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/', $workspaceOptions);
2916
                }
2917
            } else {
2918
                // When having no specific sheets, it's "TCEforms.config", when having a sheet, it's just "config"
2919
                $fieldConfiguration = $dsConf['TCEforms']['config'] ?? $dsConf['config'] ?? null;
2920
                // init with value from config for passthrough fields
2921
                if (!empty($fieldConfiguration['type']) && $fieldConfiguration['type'] === 'passthrough') {
2922
                    if (!empty($dataValues_current[$key]['vDEF'])) {
2923
                        // If there is existing value, keep it
2924
                        $dataValues[$key]['vDEF'] = $dataValues_current[$key]['vDEF'];
2925
                    } elseif (
2926
                        !empty($fieldConfiguration['default'])
2927
                        && isset($pParams[1])
2928
                        && !MathUtility::canBeInterpretedAsInteger($pParams[1])
2929
                    ) {
2930
                        // If is new record and a default is specified for field, use it.
2931
                        $dataValues[$key]['vDEF'] = $fieldConfiguration['default'];
2932
                    }
2933
                }
2934
                if (!is_array($fieldConfiguration) || !is_array($dataValues[$key])) {
2935
                    continue;
2936
                }
2937
2938
                foreach ($dataValues[$key] as $vKey => $data) {
2939
                    if ($callBackFunc) {
2940
                        if (is_object($this->callBackObj)) {
2941
                            $res = $this->callBackObj->{$callBackFunc}($pParams, $fieldConfiguration, $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
2942
                        } else {
2943
                            $res = $this->{$callBackFunc}($pParams, $fieldConfiguration, $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
2944
                        }
2945
                    } else {
2946
                        // Default
2947
                        [$CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID] = $pParams;
2948
2949
                        $additionalData = [
2950
                            'flexFormId' => $CVrecFID,
2951
                            'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
2952
                        ];
2953
2954
                        $res = $this->checkValue_SW([], $dataValues[$key][$vKey], $fieldConfiguration, $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], $CVtscPID, $additionalData);
2955
                    }
2956
                    // Adding the value:
2957
                    if (isset($res['value'])) {
2958
                        $dataValues[$key][$vKey] = $res['value'];
2959
                    }
2960
                    // Finally, check if new and old values are different (or no .vDEFbase value is found) and if so, we record the vDEF value for diff'ing.
2961
                    // We do this after $dataValues has been updated since I expect that $dataValues_current holds evaluated values from database (so this must be the right value to compare with).
2962
                    if (mb_substr($vKey, -9) !== '.vDEFbase') {
2963
                        if ($GLOBALS['TYPO3_CONF_VARS']['BE']['flexFormXMLincludeDiffBase'] && $vKey !== 'vDEF' && ((string)$dataValues[$key][$vKey] !== (string)$dataValues_current[$key][$vKey] || !isset($dataValues_current[$key][$vKey . '.vDEFbase']))) {
2964
                            // Now, check if a vDEF value is submitted in the input data, if so we expect this has been processed prior to this operation (normally the case since those fields are higher in the form) and we can use that:
2965
                            if (isset($dataValues[$key]['vDEF'])) {
2966
                                $diffValue = $dataValues[$key]['vDEF'];
2967
                            } else {
2968
                                // If not found (for translators with no access to the default language) we use the one from the current-value data set:
2969
                                $diffValue = $dataValues_current[$key]['vDEF'];
2970
                            }
2971
                            // Setting the reference value for vDEF for this translation. This will be used for translation tools to make a diff between the vDEF and vDEFbase to see if an update would be fitting.
2972
                            $dataValues[$key][$vKey . '.vDEFbase'] = $diffValue;
2973
                        }
2974
                    }
2975
                }
2976
            }
2977
        }
2978
    }
2979
2980
    /**
2981
     * Returns data for inline fields.
2982
     *
2983
     * @param array $valueArray Current value array
2984
     * @param array $tcaFieldConf TCA field config
2985
     * @param int $id Record id
2986
     * @param string $status Status string ('update' or 'new')
2987
     * @param string $table Table name, needs to be passed to \TYPO3\CMS\Core\Database\RelationHandler
2988
     * @param string $field The current field the values are modified for
2989
     * @param array $additionalData Additional data to be forwarded to sub-processors
2990
     * @return string Modified values
2991
     */
2992
    protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, array $additionalData = null)
2993
    {
2994
        $foreignTable = $tcaFieldConf['foreign_table'];
2995
        $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
2996
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
2997
        /** @var RelationHandler $dbAnalysis */
2998
        $dbAnalysis = $this->createRelationHandlerInstance();
2999
        $dbAnalysis->start(implode(',', $valueArray), $foreignTable, '', 0, $table, $tcaFieldConf);
3000
        // IRRE with a pointer field (database normalization):
3001
        if ($tcaFieldConf['foreign_field']) {
3002
            // if the record was imported, sorting was also imported, so skip this
3003
            $skipSorting = (bool)$this->callFromImpExp;
3004
            // update record in intermediate table (sorting & pointer uid to parent record)
3005
            $dbAnalysis->writeForeignField($tcaFieldConf, $id, 0, $skipSorting);
3006
            $newValue = $dbAnalysis->countItems(false);
3007
        } elseif ($this->getInlineFieldType($tcaFieldConf) === 'mm') {
3008
            // In order to fully support all the MM stuff, directly call checkValue_group_select_processDBdata instead of repeating the needed code here
3009
            $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
3010
            $newValue = $valueArray[0];
3011
        } else {
3012
            $valueArray = $dbAnalysis->getValueArray();
3013
            // Checking that the number of items is correct:
3014
            $valueArray = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
3015
            $newValue = $this->castReferenceValue(implode(',', $valueArray), $tcaFieldConf);
3016
        }
3017
        return $newValue;
3018
    }
3019
3020
    /*********************************************
3021
     *
3022
     * PROCESSING COMMANDS
3023
     *
3024
     ********************************************/
3025
    /**
3026
     * Processing the cmd-array
3027
     * See "TYPO3 Core API" for a description of the options.
3028
     *
3029
     * @return void|bool
3030
     */
3031
    public function process_cmdmap()
3032
    {
3033
        // Editing frozen:
3034
        if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
3035
            $this->newlog('All editing in this workspace has been frozen!', SystemLogErrorClassification::USER_ERROR);
3036
            return false;
3037
        }
3038
        // Hook initialization:
3039
        $hookObjectsArr = [];
3040
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
3041
            $hookObj = GeneralUtility::makeInstance($className);
3042
            if (method_exists($hookObj, 'processCmdmap_beforeStart')) {
3043
                $hookObj->processCmdmap_beforeStart($this);
3044
            }
3045
            $hookObjectsArr[] = $hookObj;
3046
        }
3047
        $pasteDatamap = [];
3048
        // Traverse command map:
3049
        foreach ($this->cmdmap as $table => $_) {
3050
            // Check if the table may be modified!
3051
            $modifyAccessList = $this->checkModifyAccessList($table);
3052
            if (!$modifyAccessList) {
3053
                $this->log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
3054
            }
3055
            // Check basic permissions and circumstances:
3056
            if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->cmdmap[$table]) || !$modifyAccessList) {
3057
                continue;
3058
            }
3059
3060
            // Traverse the command map:
3061
            foreach ($this->cmdmap[$table] as $id => $incomingCmdArray) {
3062
                if (!is_array($incomingCmdArray)) {
3063
                    continue;
3064
                }
3065
3066
                if ($table === 'pages') {
3067
                    // for commands on pages do a pagetree-refresh
3068
                    $this->pagetreeNeedsRefresh = true;
3069
                }
3070
3071
                foreach ($incomingCmdArray as $command => $value) {
3072
                    $pasteUpdate = false;
3073
                    if (is_array($value) && isset($value['action']) && $value['action'] === 'paste') {
3074
                        // Extended paste command: $command is set to "move" or "copy"
3075
                        // $value['update'] holds field/value pairs which should be updated after copy/move operation
3076
                        // $value['target'] holds original $value (target of move/copy)
3077
                        $pasteUpdate = $value['update'];
3078
                        $value = $value['target'];
3079
                    }
3080
                    foreach ($hookObjectsArr as $hookObj) {
3081
                        if (method_exists($hookObj, 'processCmdmap_preProcess')) {
3082
                            $hookObj->processCmdmap_preProcess($command, $table, $id, $value, $this, $pasteUpdate);
3083
                        }
3084
                    }
3085
                    // Init copyMapping array:
3086
                    // Must clear this array before call from here to those functions:
3087
                    // Contains mapping information between new and old id numbers.
3088
                    $this->copyMappingArray = [];
3089
                    // process the command
3090
                    $commandIsProcessed = false;
3091
                    foreach ($hookObjectsArr as $hookObj) {
3092
                        if (method_exists($hookObj, 'processCmdmap')) {
3093
                            $hookObj->processCmdmap($command, $table, $id, $value, $commandIsProcessed, $this, $pasteUpdate);
3094
                        }
3095
                    }
3096
                    // Only execute default commands if a hook hasn't been processed the command already
3097
                    if (!$commandIsProcessed) {
3098
                        $procId = $id;
3099
                        $backupUseTransOrigPointerField = $this->useTransOrigPointerField;
3100
                        // Branch, based on command
3101
                        switch ($command) {
3102
                            case 'move':
3103
                                $this->moveRecord($table, $id, $value);
3104
                                break;
3105
                            case 'copy':
3106
                                $target = $value['target'] ?? $value;
3107
                                $ignoreLocalization = (bool)($value['ignoreLocalization'] ?? false);
3108
                                if ($table === 'pages') {
3109
                                    $this->copyPages($id, $target);
3110
                                } else {
3111
                                    $this->copyRecord($table, $id, $target, true, [], '', 0, $ignoreLocalization);
3112
                                }
3113
                                $procId = $this->copyMappingArray[$table][$id];
3114
                                break;
3115
                            case 'localize':
3116
                                $this->useTransOrigPointerField = true;
3117
                                $this->localize($table, $id, $value);
3118
                                break;
3119
                            case 'copyToLanguage':
3120
                                $this->useTransOrigPointerField = false;
3121
                                $this->localize($table, $id, $value);
3122
                                break;
3123
                            case 'inlineLocalizeSynchronize':
3124
                                $this->inlineLocalizeSynchronize($table, $id, $value);
3125
                                break;
3126
                            case 'delete':
3127
                                $this->deleteAction($table, $id);
3128
                                break;
3129
                            case 'undelete':
3130
                                $this->undeleteRecord($table, $id);
3131
                                break;
3132
                        }
3133
                        $this->useTransOrigPointerField = $backupUseTransOrigPointerField;
3134
                        if (is_array($pasteUpdate)) {
3135
                            $pasteDatamap[$table][$procId] = $pasteUpdate;
3136
                        }
3137
                    }
3138
                    foreach ($hookObjectsArr as $hookObj) {
3139
                        if (method_exists($hookObj, 'processCmdmap_postProcess')) {
3140
                            $hookObj->processCmdmap_postProcess($command, $table, $id, $value, $this, $pasteUpdate, $pasteDatamap);
3141
                        }
3142
                    }
3143
                    // Merging the copy-array info together for remapping purposes.
3144
                    ArrayUtility::mergeRecursiveWithOverrule($this->copyMappingArray_merged, $this->copyMappingArray);
3145
                }
3146
            }
3147
        }
3148
        /** @var DataHandler $copyTCE */
3149
        $copyTCE = $this->getLocalTCE();
3150
        $copyTCE->start($pasteDatamap, [], $this->BE_USER);
3151
        $copyTCE->process_datamap();
3152
        $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3153
        unset($copyTCE);
3154
3155
        // Finally, before exit, check if there are ID references to remap.
3156
        // This might be the case if versioning or copying has taken place!
3157
        $this->remapListedDBRecords();
3158
        $this->processRemapStack();
3159
        foreach ($hookObjectsArr as $hookObj) {
3160
            if (method_exists($hookObj, 'processCmdmap_afterFinish')) {
3161
                $hookObj->processCmdmap_afterFinish($this);
3162
            }
3163
        }
3164
        if ($this->isOuterMostInstance()) {
3165
            $this->referenceIndexUpdater->update();
3166
            $this->processClearCacheQueue();
3167
            $this->resetNestedElementCalls();
3168
        }
3169
    }
3170
3171
    /*********************************************
3172
     *
3173
     * Cmd: Copying
3174
     *
3175
     ********************************************/
3176
    /**
3177
     * Copying a single record
3178
     *
3179
     * @param string $table Element table
3180
     * @param int $uid Element UID
3181
     * @param int $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
3182
     * @param bool $first Is a flag set, if the record copied is NOT a 'slave' to another record copied. That is, if this record was asked to be copied in the cmd-array
3183
     * @param array $overrideValues Associative array with field/value pairs to override directly. Notice; Fields must exist in the table record and NOT be among excluded fields!
3184
     * @param string $excludeFields Commalist of fields to exclude from the copy process (might get default values)
3185
     * @param int $language Language ID (from sys_language table)
3186
     * @param bool $ignoreLocalization If TRUE, any localization routine is skipped
3187
     * @return int|null ID of new record, if any
3188
     * @internal should only be used from within DataHandler
3189
     */
3190
    public function copyRecord($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '', $language = 0, $ignoreLocalization = false)
3191
    {
3192
        $uid = ($origUid = (int)$uid);
3193
        // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
3194
        if (empty($GLOBALS['TCA'][$table]) || $uid === 0) {
3195
            return null;
3196
        }
3197
        if ($this->isRecordCopied($table, $uid)) {
3198
            return null;
3199
        }
3200
3201
        // Fetch record with permission check
3202
        $row = $this->recordInfoWithPermissionCheck($table, $uid, Permission::PAGE_SHOW);
3203
3204
        // This checks if the record can be selected which is all that a copy action requires.
3205
        if ($row === false) {
3206
            $this->log($table, $uid, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy record "%s:%s" which does not exist or you do not have permission to read', -1, [$table, $uid]);
3207
            return null;
3208
        }
3209
3210
        // NOT using \TYPO3\CMS\Backend\Utility\BackendUtility::getTSCpid() because we need the real pid - not the ID of a page, if the input is a page...
3211
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
3212
3213
        // Check if table is allowed on destination page
3214
        if (!$this->isTableAllowedForThisPage($tscPID, $table)) {
3215
            $this->log($table, $uid, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to insert record "%s:%s" on a page (%s) that can\'t store record type.', -1, [$table, $uid, $tscPID]);
3216
            return null;
3217
        }
3218
3219
        $fullLanguageCheckNeeded = $table !== 'pages';
3220
        // Used to check language and general editing rights
3221
        if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded)) {
3222
            $this->log($table, $uid, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy record "%s:%s" without having permissions to do so. [' . $this->BE_USER->errorMsg . '].', -1, [$table, $uid]);
3223
            return null;
3224
        }
3225
3226
        $data = [];
3227
        $nonFields = array_unique(GeneralUtility::trimExplode(',', 'uid,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,t3ver_oid,t3ver_wsid,t3ver_state,t3ver_stage,' . $excludeFields, true));
3228
        BackendUtility::workspaceOL($table, $row, $this->BE_USER->workspace);
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type true; however, parameter $row of TYPO3\CMS\Backend\Utilit...dUtility::workspaceOL() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3228
        BackendUtility::workspaceOL($table, /** @scrutinizer ignore-type */ $row, $this->BE_USER->workspace);
Loading history...
3229
        $row = BackendUtility::purgeComputedPropertiesFromRecord($row);
3230
3231
        // Initializing:
3232
        $theNewID = StringUtility::getUniqueId('NEW');
3233
        $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3234
        $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3235
        // Getting "copy-after" fields if applicable:
3236
        $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, abs($destPid), 0) : [];
0 ignored issues
show
Bug introduced by
It seems like abs($destPid) can also be of type double; however, parameter $prevUid of TYPO3\CMS\Core\DataHandl...ixCopyAfterDuplFields() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3236
        $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, /** @scrutinizer ignore-type */ abs($destPid), 0) : [];
Loading history...
3237
        // Page TSconfig related:
3238
        $TSConfig = BackendUtility::getPagesTSconfig($tscPID)['TCEMAIN.'] ?? [];
3239
        $tE = $this->getTableEntries($table, $TSConfig);
3240
        // Traverse ALL fields of the selected record:
3241
        foreach ($row as $field => $value) {
3242
            if (!in_array($field, $nonFields, true)) {
3243
                // Get TCA configuration for the field:
3244
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3245
                // Preparation/Processing of the value:
3246
                // "pid" is hardcoded of course:
3247
                // isset() won't work here, since values can be NULL in each of the arrays
3248
                // except setDefaultOnCopyArray, since we exploded that from a string
3249
                if ($field === 'pid') {
3250
                    $value = $destPid;
3251
                } elseif (array_key_exists($field, $overrideValues)) {
3252
                    // Override value...
3253
                    $value = $overrideValues[$field];
3254
                } elseif (array_key_exists($field, $copyAfterFields)) {
3255
                    // Copy-after value if available:
3256
                    $value = $copyAfterFields[$field];
3257
                } else {
3258
                    // Hide at copy may override:
3259
                    if ($first && $field == $enableField && $GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] && !$this->neverHideAtCopy && !$tE['disableHideAtCopy']) {
3260
                        $value = 1;
3261
                    }
3262
                    // Prepend label on copy:
3263
                    if ($first && $field == $headerField && $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] && !$tE['disablePrependAtCopy']) {
3264
                        $value = $this->getCopyHeader($table, $this->resolvePid($table, $destPid), $field, $this->clearPrefixFromValue($table, $value), 0);
3265
                    }
3266
                    // Processing based on the TCA config field type (files, references, flexforms...)
3267
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $tscPID, $language);
3268
                }
3269
                // Add value to array.
3270
                $data[$table][$theNewID][$field] = $value;
3271
            }
3272
        }
3273
        // Overriding values:
3274
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3275
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3276
        }
3277
        // Setting original UID:
3278
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3279
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3280
        }
3281
        // Do the copy by simply submitting the array through DataHandler:
3282
        /** @var DataHandler $copyTCE */
3283
        $copyTCE = $this->getLocalTCE();
3284
        $copyTCE->start($data, [], $this->BE_USER);
3285
        $copyTCE->process_datamap();
3286
        // Getting the new UID:
3287
        $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID];
3288
        if ($theNewSQLID) {
3289
            $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3290
            // Keep automatically versionized record information:
3291
            if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3292
                $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3293
            }
3294
        }
3295
        $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3296
        unset($copyTCE);
3297
        if (!$ignoreLocalization && $language == 0) {
3298
            //repointing the new translation records to the parent record we just created
3299
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3300
            if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3301
                $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = 0;
3302
            }
3303
            $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
3304
        }
3305
3306
        return $theNewSQLID;
3307
    }
3308
3309
    /**
3310
     * Copying pages
3311
     * Main function for copying pages.
3312
     *
3313
     * @param int $uid Page UID to copy
3314
     * @param int $destPid Destination PID: >=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
3315
     * @internal should only be used from within DataHandler
3316
     */
3317
    public function copyPages($uid, $destPid)
3318
    {
3319
        // Initialize:
3320
        $uid = (int)$uid;
3321
        $destPid = (int)$destPid;
3322
3323
        $copyTablesAlongWithPage = $this->getAllowedTablesToCopyWhenCopyingAPage();
3324
        // Begin to copy pages if we're allowed to:
3325
        if ($this->admin || in_array('pages', $copyTablesAlongWithPage, true)) {
3326
            // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)
3327
            // This method also copies the localizations of a page
3328
            $theNewRootID = $this->copySpecificPage($uid, $destPid, $copyTablesAlongWithPage, true);
3329
            // If we're going to copy recursively
3330
            if ($theNewRootID && $this->copyTree) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $theNewRootID of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
3331
                // Get ALL subpages to copy (read-permissions are respected!):
3332
                $CPtable = $this->int_pageTreeInfo([], $uid, (int)$this->copyTree, $theNewRootID);
3333
                // Now copying the subpages:
3334
                foreach ($CPtable as $thePageUid => $thePagePid) {
3335
                    $newPid = $this->copyMappingArray['pages'][$thePagePid];
3336
                    if (isset($newPid)) {
3337
                        $this->copySpecificPage($thePageUid, $newPid, $copyTablesAlongWithPage);
3338
                    } else {
3339
                        $this->log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Something went wrong during copying branch');
3340
                        break;
3341
                    }
3342
                }
3343
            }
3344
        } else {
3345
            $this->log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy page without permission to this table');
3346
        }
3347
    }
3348
3349
    /**
3350
     * Compile a list of tables that should be copied along when a page is about to be copied.
3351
     *
3352
     * First, get the list that the user is allowed to modify (all if admin),
3353
     * and then check against a possible limitation within "DataHandler->copyWhichTables" if not set to "*"
3354
     * to limit the list further down
3355
     *
3356
     * @return array
3357
     */
3358
    protected function getAllowedTablesToCopyWhenCopyingAPage(): array
3359
    {
3360
        // Finding list of tables to copy.
3361
        // These are the tables, the user may modify
3362
        $copyTablesArray = $this->admin ? $this->compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3363
        // If not all tables are allowed then make a list of allowed tables.
3364
        // That is the tables that figure in both allowed tables AND the copyTable-list
3365
        if (strpos($this->copyWhichTables, '*') === false) {
3366
            $definedTablesToCopy = GeneralUtility::trimExplode(',', $this->copyWhichTables, true);
3367
            // Pages are always allowed
3368
            $definedTablesToCopy[] = 'pages';
3369
            $definedTablesToCopy = array_flip($definedTablesToCopy);
3370
            foreach ($copyTablesArray as $k => $table) {
3371
                if (!$table || !isset($definedTablesToCopy[$table])) {
3372
                    unset($copyTablesArray[$k]);
3373
                }
3374
            }
3375
        }
3376
        $copyTablesArray = array_unique($copyTablesArray);
3377
        return $copyTablesArray;
3378
    }
3379
    /**
3380
     * Copying a single page ($uid) to $destPid and all tables in the array copyTablesArray.
3381
     *
3382
     * @param int $uid Page uid
3383
     * @param int $destPid Destination PID: >=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
3384
     * @param array $copyTablesArray Table on pages to copy along with the page.
3385
     * @param bool $first Is a flag set, if the record copied is NOT a 'slave' to another record copied. That is, if this record was asked to be copied in the cmd-array
3386
     * @return int|null The id of the new page, if applicable.
3387
     * @internal should only be used from within DataHandler
3388
     */
3389
    public function copySpecificPage($uid, $destPid, $copyTablesArray, $first = false)
3390
    {
3391
        // Copy the page itself:
3392
        $theNewRootID = $this->copyRecord('pages', $uid, $destPid, $first);
3393
        $currentWorkspaceId = (int)$this->BE_USER->workspace;
3394
        // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3395
        if ($theNewRootID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $theNewRootID of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
3396
            foreach ($copyTablesArray as $table) {
3397
                // All records under the page is copied.
3398
                if ($table && is_array($GLOBALS['TCA'][$table]) && $table !== 'pages') {
3399
                    $fields = ['uid'];
3400
                    $languageField = null;
3401
                    $transOrigPointerField = null;
3402
                    $translationSourceField = null;
3403
                    if (BackendUtility::isTableLocalizable($table)) {
3404
                        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
3405
                        $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3406
                        $fields[] = $languageField;
3407
                        $fields[] = $transOrigPointerField;
3408
                        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3409
                            $translationSourceField = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3410
                            $fields[] = $translationSourceField;
3411
                        }
3412
                    }
3413
                    $isTableWorkspaceEnabled = BackendUtility::isTableWorkspaceEnabled($table);
3414
                    if ($isTableWorkspaceEnabled) {
3415
                        $fields[] = 't3ver_oid';
3416
                        $fields[] = 't3ver_state';
3417
                        $fields[] = 't3ver_wsid';
3418
                    }
3419
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3420
                    $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
3421
                    $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $currentWorkspaceId));
3422
                    $queryBuilder
3423
                        ->select(...$fields)
3424
                        ->from($table)
3425
                        ->where(
3426
                            $queryBuilder->expr()->eq(
3427
                                'pid',
3428
                                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3429
                            )
3430
                        );
3431
                    if (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby'])) {
3432
                        $queryBuilder->orderBy($GLOBALS['TCA'][$table]['ctrl']['sortby'], 'DESC');
3433
                    }
3434
                    $queryBuilder->addOrderBy('uid');
3435
                    try {
3436
                        $result = $queryBuilder->execute();
3437
                        $rows = [];
3438
                        $movedLiveIds = [];
3439
                        $movedLiveRecords = [];
3440
                        while ($row = $result->fetch()) {
3441
                            if ($isTableWorkspaceEnabled && (int)$row['t3ver_state'] === VersionState::MOVE_POINTER) {
3442
                                $movedLiveIds[(int)$row['t3ver_oid']] = (int)$row['uid'];
3443
                            }
3444
                            $rows[(int)$row['uid']] = $row;
3445
                        }
3446
                        // Resolve placeholders of workspace versions
3447
                        if (!empty($rows) && $currentWorkspaceId > 0 && $isTableWorkspaceEnabled) {
3448
                            // If a record was moved within the page, the PlainDataResolver needs the moved record
3449
                            // but not the original live version, otherwise the moved record is not considered at all.
3450
                            // For this reason, we find the live ids, where there was also a moved record in the SQL
3451
                            // query above in $movedLiveIds and now we removed them before handing them over to PlainDataResolver.
3452
                            // see changeContentSortingAndCopyDraftPage test
3453
                            foreach ($movedLiveIds as $liveId => $movePlaceHolderId) {
3454
                                if (isset($rows[$liveId])) {
3455
                                    $movedLiveRecords[$movePlaceHolderId] = $rows[$liveId];
3456
                                    unset($rows[$liveId]);
3457
                                }
3458
                            }
3459
                            $rows = array_reverse(
3460
                                $this->resolveVersionedRecords(
3461
                                    $table,
3462
                                    implode(',', $fields),
3463
                                    $GLOBALS['TCA'][$table]['ctrl']['sortby'],
3464
                                    array_keys($rows)
3465
                                ),
3466
                                true
3467
                            );
3468
                            foreach ($movedLiveRecords as $movePlaceHolderId => $liveRecord) {
3469
                                $rows[$movePlaceHolderId] = $liveRecord;
3470
                            }
3471
                        }
3472
                        if (is_array($rows)) {
3473
                            $languageSourceMap = [];
3474
                            $overrideValues = $translationSourceField ? [$translationSourceField => 0] : [];
3475
                            $doRemap = false;
3476
                            foreach ($rows as $row) {
3477
                                // Skip localized records that will be processed in
3478
                                // copyL10nOverlayRecords() on copying the default language record
3479
                                $transOrigPointer = $row[$transOrigPointerField];
3480
                                if ($row[$languageField] > 0 && $transOrigPointer > 0 && (isset($rows[$transOrigPointer]) || isset($movedLiveIds[$transOrigPointer]))) {
3481
                                    continue;
3482
                                }
3483
                                // Copying each of the underlying records...
3484
                                $newUid = $this->copyRecord($table, $row['uid'], $theNewRootID, false, $overrideValues);
3485
                                if ($translationSourceField) {
3486
                                    $languageSourceMap[$row['uid']] = $newUid;
3487
                                    if ($row[$languageField] > 0) {
3488
                                        $doRemap = true;
3489
                                    }
3490
                                }
3491
                            }
3492
                            if ($doRemap) {
3493
                                //remap is needed for records in non-default language records in the "free mode"
3494
                                $this->copy_remapTranslationSourceField($table, $rows, $languageSourceMap);
3495
                            }
3496
                        }
3497
                    } catch (DBALException $e) {
3498
                        $databaseErrorMessage = $e->getPrevious()->getMessage();
3499
                        $this->log($table, $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'An SQL error occurred: ' . $databaseErrorMessage);
3500
                    }
3501
                }
3502
            }
3503
            $this->processRemapStack();
3504
            return $theNewRootID;
3505
        }
3506
        return null;
3507
    }
3508
3509
    /**
3510
     * Copying records, but makes a "raw" copy of a record.
3511
     * Basically the only thing observed is field processing like the copying of files and correction of ids. All other fields are 1-1 copied.
3512
     * Technically the copy is made with THIS instance of the DataHandler class contrary to copyRecord() which creates a new instance and uses the processData() function.
3513
     * The copy is created by insertNewCopyVersion() which bypasses most of the regular input checking associated with processData() - maybe copyRecord() should even do this as well!?
3514
     * This function is used to create new versions of a record.
3515
     * NOTICE: DOES NOT CHECK PERMISSIONS to create! And since page permissions are just passed through and not changed to the user who executes the copy we cannot enforce permissions without getting an incomplete copy - unless we change permissions of course.
3516
     *
3517
     * @param string $table Element table
3518
     * @param int $uid Element UID
3519
     * @param int $pid Element PID (real PID, not checked)
3520
     * @param array $overrideArray Override array - must NOT contain any fields not in the table!
3521
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3522
     * @return int Returns the new ID of the record (if applicable)
3523
     * @internal should only be used from within DataHandler
3524
     */
3525
    public function copyRecord_raw($table, $uid, $pid, $overrideArray = [], array $workspaceOptions = [])
3526
    {
3527
        $uid = (int)$uid;
3528
        // Stop any actions if the record is marked to be deleted:
3529
        // (this can occur if IRRE elements are versionized and child elements are removed)
3530
        if ($this->isElementToBeDeleted($table, $uid)) {
3531
            return null;
3532
        }
3533
        // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3534
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
3535
            return null;
3536
        }
3537
3538
        // Fetch record with permission check
3539
        $row = $this->recordInfoWithPermissionCheck($table, $uid, Permission::PAGE_SHOW);
3540
3541
        // This checks if the record can be selected which is all that a copy action requires.
3542
        if ($row === false) {
3543
            $this->log(
3544
                $table,
3545
                $uid,
3546
                SystemLogDatabaseAction::DELETE,
3547
                0,
3548
                SystemLogErrorClassification::USER_ERROR,
3549
                'Attempt to rawcopy/versionize record which either does not exist or you don\'t have permission to read'
3550
            );
3551
            return null;
3552
        }
3553
3554
        // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3555
        $nonFields = ['uid', 'pid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody'];
3556
3557
        // Merge in override array.
3558
        $row = array_merge($row, $overrideArray);
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type true; however, parameter $array1 of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3558
        $row = array_merge(/** @scrutinizer ignore-type */ $row, $overrideArray);
Loading history...
3559
        // Traverse ALL fields of the selected record:
3560
        foreach ($row as $field => $value) {
3561
            if (!in_array($field, $nonFields, true)) {
3562
                // Get TCA configuration for the field:
3563
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3564
                if (is_array($conf)) {
3565
                    // Processing based on the TCA config field type (files, references, flexforms...)
3566
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3567
                }
3568
                // Add value to array.
3569
                $row[$field] = $value;
3570
            }
3571
        }
3572
        $row['pid'] = $pid;
3573
        // Setting original UID:
3574
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3575
            $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3576
        }
3577
        // Do the copy by internal function
3578
        $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3579
3580
        // When a record is copied in workspace (eg. to create a delete placeholder record for a live record), records
3581
        // pointing to that record need a reference index update. This is for instance the case in FAL, if a sys_file_reference
3582
        // for a eg. tt_content record is marked as deleted. The tt_content record then needs a reference index update.
3583
        // This scenario seems to currently only show up if in workspaces, so the refindex update is restricted to this for now.
3584
        if (!empty($workspaceOptions)) {
3585
            $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, (int)$row['uid'], (int)$this->BE_USER->workspace);
3586
        }
3587
3588
        if ($theNewSQLID) {
3589
            $this->dbAnalysisStoreExec();
3590
            $this->dbAnalysisStore = [];
3591
            return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3592
        }
3593
        return null;
3594
    }
3595
3596
    /**
3597
     * Inserts a record in the database, passing TCA configuration values through checkValue() but otherwise does NOTHING and checks nothing regarding permissions.
3598
     * Passes the "version" parameter to insertDB() so the copy will look like a new version in the log - should probably be changed or modified a bit for more broad usage...
3599
     *
3600
     * @param string $table Table name
3601
     * @param array $fieldArray Field array to insert as a record
3602
     * @param int $realPid The value of PID field.
3603
     * @return int Returns the new ID of the record (if applicable)
3604
     * @internal should only be used from within DataHandler
3605
     */
3606
    public function insertNewCopyVersion($table, $fieldArray, $realPid)
3607
    {
3608
        $id = StringUtility::getUniqueId('NEW');
3609
        // $fieldArray is set as current record.
3610
        // The point is that when new records are created as copies with flex type fields there might be a field containing information about which DataStructure to use and without that information the flexforms cannot be correctly processed.... This should be OK since the $checkValueRecord is used by the flexform evaluation only anyways...
3611
        $this->checkValue_currentRecord = $fieldArray;
3612
        // Makes sure that transformations aren't processed on the copy.
3613
        $backupDontProcessTransformations = $this->dontProcessTransformations;
3614
        $this->dontProcessTransformations = true;
3615
        // Traverse record and input-process each value:
3616
        foreach ($fieldArray as $field => $fieldValue) {
3617
            if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3618
                // Evaluating the value.
3619
                $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0, $fieldArray);
3620
                if (isset($res['value'])) {
3621
                    $fieldArray[$field] = $res['value'];
3622
                }
3623
            }
3624
        }
3625
        // System fields being set:
3626
        if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3627
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3628
        }
3629
        if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3630
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3631
        }
3632
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3633
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3634
        }
3635
        // Finally, insert record:
3636
        $this->insertDB($table, $id, $fieldArray, true);
3637
        // Resets dontProcessTransformations to the previous state.
3638
        $this->dontProcessTransformations = $backupDontProcessTransformations;
3639
        // Return new id:
3640
        return $this->substNEWwithIDs[$id];
3641
    }
3642
3643
    /**
3644
     * Processing/Preparing content for copyRecord() function
3645
     *
3646
     * @param string $table Table name
3647
     * @param int $uid Record uid
3648
     * @param string $field Field name being processed
3649
     * @param string $value Input value to be processed.
3650
     * @param array $row Record array
3651
     * @param array $conf TCA field configuration
3652
     * @param int $realDestPid Real page id (pid) the record is copied to
3653
     * @param int $language Language ID (from sys_language table) used in the duplicated record
3654
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3655
     * @return array|string
3656
     * @internal
3657
     * @see copyRecord()
3658
     */
3659
    public function copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = [])
3660
    {
3661
        $inlineSubType = $this->getInlineFieldType($conf);
3662
        // Get the localization mode for the current (parent) record (keep|select):
3663
        // Register if there are references to take care of or MM is used on an inline field (no change to value):
3664
        if ($this->isReferenceField($conf) || $inlineSubType === 'mm') {
3665
            $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language);
3666
        } elseif ($inlineSubType !== false) {
3667
            $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions);
3668
        }
3669
        // For "flex" fieldtypes we need to traverse the structure for two reasons: If there are file references they have to be prepended with absolute paths and if there are database reference they MIGHT need to be remapped (still done in remapListedDBRecords())
3670
        if ($conf['type'] === 'flex') {
3671
            // Get current value array:
3672
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
3673
            $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
3674
                ['config' => $conf],
3675
                $table,
3676
                $field,
3677
                $row
3678
            );
3679
            $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
3680
            $currentValueArray = GeneralUtility::xml2array($value);
3681
            // Traversing the XML structure, processing files:
3682
            if (is_array($currentValueArray)) {
3683
                $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
3684
                // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3685
                $value = $currentValueArray;
3686
            }
3687
        }
3688
        return $value;
3689
    }
3690
3691
    /**
3692
     * Processes the children of an MM relation field (select, group, inline) when the parent record is copied.
3693
     *
3694
     * @param string $table
3695
     * @param int $uid
3696
     * @param string $field
3697
     * @param mixed $value
3698
     * @param array $conf
3699
     * @param string $language
3700
     * @return mixed
3701
     */
3702
    protected function copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language)
3703
    {
3704
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
3705
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
3706
        $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3707
        $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3708
        // Localize referenced records of select fields:
3709
        $localizingNonManyToManyFieldReferences = empty($mmTable) && $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3710
        /** @var RelationHandler $dbAnalysis */
3711
        $dbAnalysis = $this->createRelationHandlerInstance();
3712
        $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3713
        $purgeItems = false;
3714
        if ($language > 0 && $localizingNonManyToManyFieldReferences) {
3715
            foreach ($dbAnalysis->itemArray as $index => $item) {
3716
                // Since select fields can reference many records, check whether there's already a localization:
3717
                $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], $language);
0 ignored issues
show
Bug introduced by
$language of type string is incompatible with the type integer expected by parameter $language of TYPO3\CMS\Backend\Utilit...getRecordLocalization(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3717
                $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3718
                if ($recordLocalization) {
3719
                    $dbAnalysis->itemArray[$index]['id'] = $recordLocalization[0]['uid'];
3720
                } elseif ($this->isNestedElementCallRegistered($item['table'], $item['id'], 'localize-' . (string)$language) === false) {
3721
                    $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], $language);
0 ignored issues
show
Bug introduced by
$language of type string is incompatible with the type integer expected by parameter $language of TYPO3\CMS\Core\DataHandl...DataHandler::localize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3721
                    $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3722
                }
3723
            }
3724
            $purgeItems = true;
3725
        }
3726
3727
        if ($purgeItems || $mmTable) {
3728
            $dbAnalysis->purgeItemArray();
3729
            $value = implode(',', $dbAnalysis->getValueArray($prependName));
0 ignored issues
show
Bug introduced by
It seems like $prependName can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\...andler::getValueArray() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3729
            $value = implode(',', $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName));
Loading history...
3730
        }
3731
        // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3732
        if ($value) {
3733
            $this->registerDBList[$table][$uid][$field] = $value;
3734
        }
3735
3736
        return $value;
3737
    }
3738
3739
    /**
3740
     * Processes child records in an inline (IRRE) element when the parent record is copied.
3741
     *
3742
     * @param string $table
3743
     * @param int $uid
3744
     * @param string $field
3745
     * @param mixed $value
3746
     * @param array $row
3747
     * @param array $conf
3748
     * @param int $realDestPid
3749
     * @param string $language
3750
     * @param array $workspaceOptions
3751
     * @return string
3752
     */
3753
    protected function copyRecord_processInline(
3754
        $table,
3755
        $uid,
3756
        $field,
3757
        $value,
3758
        $row,
3759
        $conf,
3760
        $realDestPid,
3761
        $language,
3762
        array $workspaceOptions
3763
    ) {
3764
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3765
        /** @var RelationHandler $dbAnalysis */
3766
        $dbAnalysis = $this->createRelationHandlerInstance();
3767
        $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3768
        // Walk through the items, copy them and remember the new id:
3769
        foreach ($dbAnalysis->itemArray as $k => $v) {
3770
            $newId = null;
3771
            // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3772
            if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3773
                // Children should be localized when the parent gets localized the first time, just do it:
3774
                $newId = $this->localize($v['table'], $v['id'], $language);
0 ignored issues
show
Bug introduced by
$language of type string is incompatible with the type integer expected by parameter $language of TYPO3\CMS\Core\DataHandl...DataHandler::localize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3774
                $newId = $this->localize($v['table'], $v['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3775
            } else {
3776
                if (!MathUtility::canBeInterpretedAsInteger($realDestPid)) {
3777
                    $newId = $this->copyRecord($v['table'], $v['id'], -$v['id']);
3778
                // If the destination page id is a NEW string, keep it on the same page
3779
                } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3780
                    // A filled $workspaceOptions indicated that this call
3781
                    // has it's origin in previous versionizeRecord() processing
3782
                    if (!empty($workspaceOptions)) {
3783
                        // Versions use live default id, thus the "new"
3784
                        // id is the original live default child record
3785
                        $newId = $v['id'];
3786
                        $this->versionizeRecord(
3787
                            $v['table'],
3788
                            $v['id'],
3789
                            $workspaceOptions['label'] ?? 'Auto-created for WS #' . $this->BE_USER->workspace,
3790
                            $workspaceOptions['delete'] ?? false
3791
                        );
3792
                    // Otherwise just use plain copyRecord() to create placeholders etc.
3793
                    } else {
3794
                        // If a record has been copied already during this request,
3795
                        // prevent superfluous duplication and use the existing copy
3796
                        if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3797
                            $newId = $this->copyMappingArray[$v['table']][$v['id']];
3798
                        } else {
3799
                            $newId = $this->copyRecord($v['table'], $v['id'], $realDestPid);
3800
                        }
3801
                    }
3802
                } elseif ($this->BE_USER->workspace > 0 && !BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3803
                    // We are in workspace context creating a new parent version and have a child table
3804
                    // that is not workspace aware. We don't do anything with this child.
3805
                    continue;
3806
                } else {
3807
                    // If a record has been copied already during this request,
3808
                    // prevent superfluous duplication and use the existing copy
3809
                    if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3810
                        $newId = $this->copyMappingArray[$v['table']][$v['id']];
3811
                    } else {
3812
                        $newId = $this->copyRecord_raw($v['table'], $v['id'], $realDestPid, [], $workspaceOptions);
3813
                    }
3814
                }
3815
            }
3816
            // If the current field is set on a page record, update the pid of related child records:
3817
            if ($table === 'pages') {
3818
                $this->registerDBPids[$v['table']][$v['id']] = $uid;
3819
            } elseif (isset($this->registerDBPids[$table][$uid])) {
3820
                $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][$uid];
3821
            }
3822
            $dbAnalysis->itemArray[$k]['id'] = $newId;
3823
        }
3824
        // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
3825
        $value = implode(',', $dbAnalysis->getValueArray());
3826
        $this->registerDBList[$table][$uid][$field] = $value;
3827
3828
        return $value;
3829
    }
3830
3831
    /**
3832
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
3833
     *
3834
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
3835
     * @param array $dsConf TCA field configuration (from Data Structure XML)
3836
     * @param string $dataValue The value of the flexForm field
3837
     * @param string $_1 Not used.
3838
     * @param string $_2 Not used.
3839
     * @param string $_3 Not used.
3840
     * @param array $workspaceOptions
3841
     * @return array Result array with key "value" containing the value of the processing.
3842
     * @see copyRecord()
3843
     * @see checkValue_flex_procInData_travDS()
3844
     * @internal should only be used from within DataHandler
3845
     */
3846
    public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
3847
    {
3848
        // Extract parameters:
3849
        [$table, $uid, $field, $realDestPid] = $pParams;
3850
        // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
3851
        if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
3852
            $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, [], $dsConf, $realDestPid, 0, $workspaceOptions);
3853
            $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
3854
        }
3855
        // Return
3856
        return ['value' => $dataValue];
3857
    }
3858
3859
    /**
3860
     * Find l10n-overlay records and perform the requested copy action for these records.
3861
     *
3862
     * @param string $table Record Table
3863
     * @param string $uid UID of the record in the default language
3864
     * @param string $destPid Position to copy to
3865
     * @param bool $first
3866
     * @param array $overrideValues
3867
     * @param string $excludeFields
3868
     * @internal should only be used from within DataHandler
3869
     */
3870
    public function copyL10nOverlayRecords($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '')
3871
    {
3872
        // There's no need to perform this for tables that are not localizable
3873
        if (!BackendUtility::isTableLocalizable($table)) {
3874
            return;
3875
        }
3876
3877
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3878
        $queryBuilder->getRestrictions()
3879
            ->removeAll()
3880
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3881
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
3882
3883
        $queryBuilder->select('*')
3884
            ->from($table)
3885
            ->where(
3886
                $queryBuilder->expr()->eq(
3887
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3888
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
3889
                )
3890
            );
3891
3892
        // Never copy the actual placeholders around, as the newly copied records are
3893
        // always created as new record / new placeholder pairs
3894
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
3895
            $queryBuilder->andWhere(
3896
                $queryBuilder->expr()->neq(
3897
                    't3ver_state',
3898
                    VersionState::DELETE_PLACEHOLDER
3899
                )
3900
            );
3901
        }
3902
3903
        // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
3904
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
0 ignored issues
show
Bug introduced by
$destPid of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...:getTSconfig_pidValue(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3904
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, /** @scrutinizer ignore-type */ $destPid);
Loading history...
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Utilit...:getTSconfig_pidValue(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3904
        $tscPID = BackendUtility::getTSconfig_pidValue($table, /** @scrutinizer ignore-type */ $uid, $destPid);
Loading history...
3905
        // Get the localized records to be copied
3906
        $l10nRecords = $queryBuilder->execute()->fetchAll();
3907
        if (is_array($l10nRecords)) {
3908
            $localizedDestPids = [];
3909
            // If $destPid < 0, then it is the uid of the original language record we are inserting after
3910
            if ($destPid < 0) {
3911
                // Get the localized records of the record we are inserting after
3912
                $queryBuilder->setParameter('pointer', abs($destPid), \PDO::PARAM_INT);
3913
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
3914
                // Index the localized record uids by language
3915
                if (is_array($destL10nRecords)) {
3916
                    foreach ($destL10nRecords as $record) {
3917
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
3918
                    }
3919
                }
3920
            }
3921
            $languageSourceMap = [
3922
                $uid => $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
3923
            ];
3924
            // Copy the localized records after the corresponding localizations of the destination record
3925
            foreach ($l10nRecords as $record) {
3926
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
3927
                if ($localizedDestPid < 0) {
3928
                    $newUid = $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
3929
                } else {
3930
                    $newUid = $this->copyRecord($table, $record['uid'], $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
0 ignored issues
show
Bug introduced by
It seems like $destPid < 0 ? $tscPID : $destPid can also be of type string; however, parameter $destPid of TYPO3\CMS\Core\DataHandl...taHandler::copyRecord() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3930
                    $newUid = $this->copyRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
Loading history...
3931
                }
3932
                $languageSourceMap[$record['uid']] = $newUid;
3933
            }
3934
            $this->copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap);
3935
        }
3936
    }
3937
3938
    /**
3939
     * Remap languageSource field to uids of newly created records
3940
     *
3941
     * @param string $table Table name
3942
     * @param array $l10nRecords array of localized records from the page we're copying from (source records)
3943
     * @param array $languageSourceMap array mapping source records uids to newly copied uids
3944
     */
3945
    protected function copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap)
3946
    {
3947
        if (empty($GLOBALS['TCA'][$table]['ctrl']['translationSource']) || empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
3948
            return;
3949
        }
3950
        $translationSourceFieldName = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3951
        $translationParentFieldName = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3952
3953
        //We can avoid running these update queries by sorting the $l10nRecords by languageSource dependency (in copyL10nOverlayRecords)
3954
        //and first copy records depending on default record (and map the field).
3955
        foreach ($l10nRecords as $record) {
3956
            $oldSourceUid = $record[$translationSourceFieldName];
3957
            if ($oldSourceUid <= 0 && $record[$translationParentFieldName] > 0) {
3958
                //BC fix - in connected mode 'translationSource' field should not be 0
3959
                $oldSourceUid = $record[$translationParentFieldName];
3960
            }
3961
            if ($oldSourceUid > 0) {
3962
                if (empty($languageSourceMap[$oldSourceUid])) {
3963
                    // we don't have mapping information available e.g when copyRecord returned null
3964
                    continue;
3965
                }
3966
                $newFieldValue = $languageSourceMap[$oldSourceUid];
3967
                $updateFields = [
3968
                    $translationSourceFieldName => $newFieldValue
3969
                ];
3970
                GeneralUtility::makeInstance(ConnectionPool::class)
3971
                    ->getConnectionForTable($table)
3972
                    ->update($table, $updateFields, ['uid' => (int)$languageSourceMap[$record['uid']]]);
3973
                if ($this->BE_USER->workspace > 0) {
3974
                    GeneralUtility::makeInstance(ConnectionPool::class)
3975
                        ->getConnectionForTable($table)
3976
                        ->update($table, $updateFields, ['t3ver_oid' => (int)$languageSourceMap[$record['uid']], 't3ver_wsid' => $this->BE_USER->workspace]);
3977
                }
3978
            }
3979
        }
3980
    }
3981
3982
    /*********************************************
3983
     *
3984
     * Cmd: Moving, Localizing
3985
     *
3986
     ********************************************/
3987
    /**
3988
     * Moving single records
3989
     *
3990
     * @param string $table Table name to move
3991
     * @param int $uid Record uid to move
3992
     * @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
3993
     * @internal should only be used from within DataHandler
3994
     */
3995
    public function moveRecord($table, $uid, $destPid)
3996
    {
3997
        if (!$GLOBALS['TCA'][$table]) {
3998
            return;
3999
        }
4000
4001
        // In case the record to be moved turns out to be an offline version,
4002
        // we have to find the live version and work on that one.
4003
        if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $uid, 'uid')) {
4004
            $uid = $lookForLiveVersion['uid'];
4005
        }
4006
        // Initialize:
4007
        $destPid = (int)$destPid;
4008
        // Get this before we change the pid (for logging)
4009
        $propArr = $this->getRecordProperties($table, $uid);
4010
        $moveRec = $this->getRecordProperties($table, $uid, true);
4011
        // This is the actual pid of the moving to destination
4012
        $resolvedPid = $this->resolvePid($table, $destPid);
4013
        // Finding out, if the record may be moved from where it is. If the record is a non-page, then it depends on edit-permissions.
4014
        // If the record is a page, then there are two options: If the page is moved within itself,
4015
        // (same pid) it's edit-perms of the pid. If moved to another place then its both delete-perms of the pid and new-page perms on the destination.
4016
        if ($table !== 'pages' || $resolvedPid == $moveRec['pid']) {
4017
            // Edit rights for the record...
4018
            $mayMoveAccess = $this->checkRecordUpdateAccess($table, $uid);
4019
        } else {
4020
            $mayMoveAccess = $this->doesRecordExist($table, $uid, Permission::PAGE_DELETE);
4021
        }
4022
        // Finding out, if the record may be moved TO another place. Here we check insert-rights (non-pages = edit, pages = new),
4023
        // unless the pages are moved on the same pid, then edit-rights are checked
4024
        if ($table !== 'pages' || $resolvedPid != $moveRec['pid']) {
4025
            // Insert rights for the record...
4026
            $mayInsertAccess = $this->checkRecordInsertAccess($table, $resolvedPid, SystemLogDatabaseAction::MOVE);
4027
        } else {
4028
            $mayInsertAccess = $this->checkRecordUpdateAccess($table, $uid);
4029
        }
4030
        // Checking if there is anything else disallowing moving the record by checking if editing is allowed
4031
        $fullLanguageCheckNeeded = $table !== 'pages';
4032
        $mayEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded);
4033
        // If moving is allowed, begin the processing:
4034
        if (!$mayEditAccess) {
4035
            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record "%s" (%s) without having permissions to do so. [' . $this->BE_USER->errorMsg . ']', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4036
            return;
4037
        }
4038
4039
        if (!$mayMoveAccess) {
4040
            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record \'%s\' (%s) without having permissions to do so.', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4041
            return;
4042
        }
4043
4044
        if (!$mayInsertAccess) {
4045
            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record \'%s\' (%s) without having permissions to insert.', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4046
            return;
4047
        }
4048
4049
        $recordWasMoved = false;
4050
        // Move the record via a hook, used e.g. for versioning
4051
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
4052
            $hookObj = GeneralUtility::makeInstance($className);
4053
            if (method_exists($hookObj, 'moveRecord')) {
4054
                $hookObj->moveRecord($table, $uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, $this);
4055
            }
4056
        }
4057
        // Move the record if a hook hasn't moved it yet
4058
        if (!$recordWasMoved) {
0 ignored issues
show
introduced by
The condition $recordWasMoved is always false.
Loading history...
4059
            $this->moveRecord_raw($table, $uid, $destPid);
4060
        }
4061
    }
4062
4063
    /**
4064
     * Moves a record without checking security of any sort.
4065
     * USE ONLY INTERNALLY
4066
     *
4067
     * @param string $table Table name to move
4068
     * @param int $uid Record uid to move
4069
     * @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
4070
     * @see moveRecord()
4071
     * @internal should only be used from within DataHandler
4072
     */
4073
    public function moveRecord_raw($table, $uid, $destPid)
4074
    {
4075
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
4076
        $origDestPid = $destPid;
4077
        // This is the actual pid of the moving to destination
4078
        $resolvedPid = $this->resolvePid($table, $destPid);
4079
        // Checking if the pid is negative, but no sorting row is defined. In that case, find the correct pid.
4080
        // Basically this check make the error message 4-13 meaning less... But you can always remove this check if you
4081
        // prefer the error instead of a no-good action (which is to move the record to its own page...)
4082
        if (($destPid < 0 && !$sortColumn) || $destPid >= 0) {
4083
            $destPid = $resolvedPid;
4084
        }
4085
        // Get this before we change the pid (for logging)
4086
        $propArr = $this->getRecordProperties($table, $uid);
4087
        $moveRec = $this->getRecordProperties($table, $uid, true);
4088
        // Prepare user defined objects (if any) for hooks which extend this function:
4089
        $hookObjectsArr = [];
4090
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
4091
            $hookObjectsArr[] = GeneralUtility::makeInstance($className);
4092
        }
4093
        // Timestamp field:
4094
        $updateFields = [];
4095
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4096
            $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4097
        }
4098
4099
        // Check if this is a translation of a page, if so then it just needs to be kept "sorting" in sync
4100
        // Usually called from moveL10nOverlayRecords()
4101
        if ($table === 'pages') {
4102
            $defaultLanguagePageId = $this->getDefaultLanguagePageId((int)$uid);
4103
            if ($defaultLanguagePageId !== (int)$uid) {
4104
                $originalTranslationRecord = $this->recordInfo($table, $defaultLanguagePageId, 'pid,' . $sortColumn);
4105
                $updateFields[$sortColumn] = $originalTranslationRecord[$sortColumn];
4106
                // Ensure that the PID is always the same as the default language page
4107
                $destPid = $originalTranslationRecord['pid'];
4108
            }
4109
        }
4110
4111
        // Insert as first element on page (where uid = $destPid)
4112
        if ($destPid >= 0) {
4113
            if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4114
                // Clear cache before moving
4115
                [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4115
                [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4116
                $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4117
                // Setting PID
4118
                $updateFields['pid'] = $destPid;
4119
                // Table is sorted by 'sortby'
4120
                if ($sortColumn && !isset($updateFields[$sortColumn])) {
4121
                    $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4122
                    $updateFields[$sortColumn] = $sortNumber;
4123
                }
4124
                // Check for child records that have also to be moved
4125
                $this->moveRecord_procFields($table, $uid, $destPid);
4126
                // Create query for update:
4127
                GeneralUtility::makeInstance(ConnectionPool::class)
4128
                    ->getConnectionForTable($table)
4129
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4130
                // Check for the localizations of that element
4131
                $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4132
                // Call post processing hooks:
4133
                foreach ($hookObjectsArr as $hookObj) {
4134
                    if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4135
                        $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4136
                    }
4137
                }
4138
4139
                $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4140
                if ($this->enableLogging) {
4141
                    // Logging...
4142
                    $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4143
                    if ($destPid != $propArr['pid']) {
4144
                        // Logged to old page
4145
                        $newPropArr = $this->getRecordProperties($table, $uid);
4146
                        $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4147
                        $this->log($table, $uid, SystemLogDatabaseAction::MOVE, $destPid, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, [$propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']], $propArr['pid']);
4148
                        // Logged to new page
4149
                        $this->log($table, $uid, SystemLogDatabaseAction::MOVE, $destPid, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4150
                    } else {
4151
                        // Logged to new page
4152
                        $this->log($table, $uid, SystemLogDatabaseAction::MOVE, $destPid, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4153
                    }
4154
                }
4155
                // Clear cache after moving
4156
                $this->registerRecordIdForPageCacheClearing($table, $uid);
4157
                $this->fixUniqueInPid($table, $uid);
4158
                $this->fixUniqueInSite($table, (int)$uid);
4159
                if ($table === 'pages') {
4160
                    $this->fixUniqueInSiteForSubpages((int)$uid);
4161
                }
4162
            } elseif ($this->enableLogging) {
4163
                $destPropArr = $this->getRecordProperties('pages', $destPid);
4164
                $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move page \'%s\' (%s) to inside of its own rootline (at page \'%s\' (%s))', 10, [$propArr['header'], $uid, $destPropArr['header'], $destPid], $propArr['pid']);
4165
            }
4166
        } elseif ($sortColumn) {
4167
            // Put after another record
4168
            // Table is being sorted
4169
            // Save the position to which the original record is requested to be moved
4170
            $originalRecordDestinationPid = $destPid;
4171
            $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4172
            // Setting the destPid to the new pid of the record.
4173
            $destPid = $sortInfo['pid'];
4174
            // If not an array, there was an error (which is already logged)
4175
            if (is_array($sortInfo)) {
4176
                if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4177
                    // clear cache before moving
4178
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4179
                    // We now update the pid and sortnumber (if not set for page translations)
4180
                    $updateFields['pid'] = $destPid;
4181
                    if (!isset($updateFields[$sortColumn])) {
4182
                        $updateFields[$sortColumn] = $sortInfo['sortNumber'];
4183
                    }
4184
                    // Check for child records that have also to be moved
4185
                    $this->moveRecord_procFields($table, $uid, $destPid);
4186
                    // Create query for update:
4187
                    GeneralUtility::makeInstance(ConnectionPool::class)
4188
                        ->getConnectionForTable($table)
4189
                        ->update($table, $updateFields, ['uid' => (int)$uid]);
4190
                    // Check for the localizations of that element
4191
                    $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4192
                    // Call post processing hooks:
4193
                    foreach ($hookObjectsArr as $hookObj) {
4194
                        if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4195
                            $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4196
                        }
4197
                    }
4198
                    $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4199
                    if ($this->enableLogging) {
4200
                        // Logging...
4201
                        $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4202
                        if ($destPid != $propArr['pid']) {
4203
                            // Logged to old page
4204
                            $newPropArr = $this->getRecordProperties($table, $uid);
4205
                            $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4206
                            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, [$propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']], $propArr['pid']);
4207
                            // Logged to old page
4208
                            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4209
                        } else {
4210
                            // Logged to old page
4211
                            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4212
                        }
4213
                    }
4214
                    // Clear cache after moving
4215
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4216
                    $this->fixUniqueInPid($table, $uid);
4217
                    $this->fixUniqueInSite($table, (int)$uid);
4218
                    if ($table === 'pages') {
4219
                        $this->fixUniqueInSiteForSubpages((int)$uid);
4220
                    }
4221
                } elseif ($this->enableLogging) {
4222
                    $destPropArr = $this->getRecordProperties('pages', $destPid);
4223
                    $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move page \'%s\' (%s) to inside of its own rootline (at page \'%s\' (%s))', 10, [$propArr['header'], $uid, $destPropArr['header'], $destPid], $propArr['pid']);
4224
                }
4225
            } else {
4226
                $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record \'%s\' (%s) to after another record, although the table has no sorting row.', 13, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4227
            }
4228
        }
4229
    }
4230
4231
    /**
4232
     * Walk through all fields of the moved record and look for children of e.g. the inline type.
4233
     * If child records are found, they are also move to the new $destPid.
4234
     *
4235
     * @param string $table Record Table
4236
     * @param int $uid Record UID
4237
     * @param int $destPid Position to move to
4238
     * @internal should only be used from within DataHandler
4239
     */
4240
    public function moveRecord_procFields($table, $uid, $destPid)
4241
    {
4242
        $row = BackendUtility::getRecordWSOL($table, $uid);
4243
        if (is_array($row) && (int)$destPid !== (int)$row['pid']) {
4244
            $conf = $GLOBALS['TCA'][$table]['columns'];
4245
            foreach ($row as $field => $value) {
4246
                $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4247
            }
4248
        }
4249
    }
4250
4251
    /**
4252
     * Move child records depending on the field type of the parent record.
4253
     *
4254
     * @param string $table Record Table
4255
     * @param string $uid Record UID
4256
     * @param string $destPid Position to move to
4257
     * @param string $field Record field
4258
     * @param string $value Record field value
4259
     * @param array $conf TCA configuration of current field
4260
     * @internal should only be used from within DataHandler
4261
     */
4262
    public function moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
4263
    {
4264
        $dbAnalysis = null;
4265
        if ($conf['type'] === 'inline') {
4266
            $foreign_table = $conf['foreign_table'];
4267
            $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4268
            if ($foreign_table && $moveChildrenWithParent) {
4269
                $inlineType = $this->getInlineFieldType($conf);
4270
                if ($inlineType === 'list' || $inlineType === 'field') {
4271
                    if ($table === 'pages') {
4272
                        // If the inline elements are related to a page record,
4273
                        // make sure they reside at that page and not at its parent
4274
                        $destPid = $uid;
4275
                    }
4276
                    $dbAnalysis = $this->createRelationHandlerInstance();
4277
                    $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
0 ignored issues
show
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $MMuid of TYPO3\CMS\Core\Database\RelationHandler::start(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4277
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
4278
                }
4279
            }
4280
        }
4281
        // Move the records
4282
        if (isset($dbAnalysis)) {
4283
            // Moving records to a positive destination will insert each
4284
            // record at the beginning, thus the order is reversed here:
4285
            foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4286
                $this->moveRecord($v['table'], $v['id'], $destPid);
0 ignored issues
show
Bug introduced by
$destPid of type string is incompatible with the type integer expected by parameter $destPid of TYPO3\CMS\Core\DataHandl...taHandler::moveRecord(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4286
                $this->moveRecord($v['table'], $v['id'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4287
            }
4288
        }
4289
    }
4290
4291
    /**
4292
     * Find l10n-overlay records and perform the requested move action for these records.
4293
     *
4294
     * @param string $table Record Table
4295
     * @param string $uid Record UID
4296
     * @param string $destPid Position to move to
4297
     * @param string $originalRecordDestinationPid Position to move the original record to
4298
     * @internal should only be used from within DataHandler
4299
     */
4300
    public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4301
    {
4302
        // There's no need to perform this for non-localizable tables
4303
        if (!BackendUtility::isTableLocalizable($table)) {
4304
            return;
4305
        }
4306
4307
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4308
        $queryBuilder->getRestrictions()
4309
            ->removeAll()
4310
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4311
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->BE_USER->workspace));
4312
4313
        $l10nRecords = $queryBuilder->select('*')
4314
            ->from($table)
4315
            ->where(
4316
                $queryBuilder->expr()->eq(
4317
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
4318
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
4319
                )
4320
            )
4321
            ->execute()
4322
            ->fetchAll();
4323
4324
        if (is_array($l10nRecords)) {
4325
            $localizedDestPids = [];
4326
            // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4327
            if ($originalRecordDestinationPid < 0) {
4328
                // Get the localized records of the record we are inserting after
4329
                $queryBuilder->setParameter('pointer', abs($originalRecordDestinationPid), \PDO::PARAM_INT);
4330
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
4331
                // Index the localized record uids by language
4332
                if (is_array($destL10nRecords)) {
4333
                    foreach ($destL10nRecords as $record) {
4334
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4335
                    }
4336
                }
4337
            }
4338
            // Move the localized records after the corresponding localizations of the destination record
4339
            foreach ($l10nRecords as $record) {
4340
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4341
                if ($localizedDestPid < 0) {
4342
                    $this->moveRecord($table, $record['uid'], $localizedDestPid);
4343
                } else {
4344
                    $this->moveRecord($table, $record['uid'], $destPid);
0 ignored issues
show
Bug introduced by
$destPid of type string is incompatible with the type integer expected by parameter $destPid of TYPO3\CMS\Core\DataHandl...taHandler::moveRecord(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4344
                    $this->moveRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4345
                }
4346
            }
4347
        }
4348
    }
4349
4350
    /**
4351
     * Localizes a record to another system language
4352
     *
4353
     * @param string $table Table name
4354
     * @param int $uid Record uid (to be localized)
4355
     * @param int $language Language ID (from sys_language table)
4356
     * @return int|bool The uid (int) of the new translated record or FALSE (bool) if something went wrong
4357
     * @internal should only be used from within DataHandler
4358
     */
4359
    public function localize($table, $uid, $language)
4360
    {
4361
        $newId = false;
4362
        $uid = (int)$uid;
4363
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize-' . (string)$language) !== false) {
4364
            return false;
4365
        }
4366
4367
        $this->registerNestedElementCall($table, $uid, 'localize-' . (string)$language);
4368
        if (!$GLOBALS['TCA'][$table]['ctrl']['languageField'] || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
4369
            $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table ' . $table, SystemLogErrorClassification::USER_ERROR);
4370
            return false;
4371
        }
4372
        $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4373
        if (!$langRec) {
4374
            $this->newlog('Sys language UID "' . $language . '" not found valid!', SystemLogErrorClassification::USER_ERROR);
4375
            return false;
4376
        }
4377
4378
        if (!$this->doesRecordExist($table, $uid, Permission::PAGE_SHOW)) {
4379
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' without permission.', SystemLogErrorClassification::USER_ERROR);
4380
            return false;
4381
        }
4382
4383
        // Getting workspace overlay if possible - this will localize versions in workspace if any
4384
        $row = BackendUtility::getRecordWSOL($table, $uid);
4385
        if (!is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
4386
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' that did not exist!', SystemLogErrorClassification::USER_ERROR);
4387
            return false;
4388
        }
4389
4390
        // Make sure that records which are translated from another language than the default language have a correct
4391
        // localization source set themselves, before translating them to another language.
4392
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4393
            && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0) {
4394
            $localizationParentRecord = BackendUtility::getRecord(
4395
                $table,
4396
                $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4397
            );
4398
            if ((int)$localizationParentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4399
                $this->newlog('Localization failed; Source record ' . $table . ':' . $localizationParentRecord['uid'] . ' contained a reference to an original record that is not a default record (which is strange)!', SystemLogErrorClassification::USER_ERROR);
4400
                return false;
4401
            }
4402
        }
4403
4404
        // Default language records must never have a localization parent as they are the origin of any translation.
4405
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4406
            && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4407
            $this->newlog('Localization failed; Source record ' . $table . ':' . $row['uid'] . ' contained a reference to an original default record but is a default record itself (which is strange)!', SystemLogErrorClassification::USER_ERROR);
4408
            return false;
4409
        }
4410
4411
        $recordLocalizations = BackendUtility::getRecordLocalization($table, $uid, $language, 'AND pid=' . (int)$row['pid']);
4412
4413
        if (!empty($recordLocalizations)) {
4414
            $this->newlog(sprintf(
4415
                'Localization failed: there already are localizations (%s) for language %d of the "%s" record %d!',
4416
                implode(', ', array_column($recordLocalizations, 'uid')),
4417
                $language,
4418
                $table,
4419
                $uid
4420
            ), 1);
4421
            return false;
4422
        }
4423
4424
        // Initialize:
4425
        $overrideValues = [];
4426
        // Set override values:
4427
        $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $langRec['uid'];
4428
        // If the translated record is a default language record, set it's uid as localization parent of the new record.
4429
        // If translating from any other language, no override is needed; we just can copy the localization parent of
4430
        // the original record (which is pointing to the correspondent default language record) to the new record.
4431
        // In copy / free mode the TransOrigPointer field is always set to 0, as no connection to the localization parent is wanted in that case.
4432
        // For pages, there is no "copy/free mode".
4433
        if (($this->useTransOrigPointerField || $table === 'pages') && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4434
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $uid;
4435
        } elseif (!$this->useTransOrigPointerField) {
4436
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = 0;
4437
        }
4438
        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
4439
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = $uid;
4440
        }
4441
        // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4442
        if (isset($GLOBALS['TCA'][$table]['ctrl']['type'])) {
4443
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4444
        }
4445
        // Set exclude Fields:
4446
        foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fCfg) {
4447
            $translateToMsg = '';
4448
            // Check if we are just prefixing:
4449
            if ($fCfg['l10n_mode'] === 'prefixLangTitle') {
4450
                if (($fCfg['config']['type'] === 'text' || $fCfg['config']['type'] === 'input') && (string)$row[$fN] !== '') {
4451
                    [$tscPID] = BackendUtility::getTSCpid($table, $uid, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4451
                    [$tscPID] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4452
                    $TSConfig = BackendUtility::getPagesTSconfig($tscPID)['TCEMAIN.'] ?? [];
4453
                    $tE = $this->getTableEntries($table, $TSConfig);
4454
                    if (!empty($TSConfig['translateToMessage']) && !$tE['disablePrependAtCopy']) {
4455
                        $translateToMsg = $this->getLanguageService()->sL($TSConfig['translateToMessage']);
4456
                        $translateToMsg = @sprintf($translateToMsg, $langRec['title']);
4457
                    }
4458
4459
                    foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processTranslateToClass'] ?? [] as $className) {
4460
                        $hookObj = GeneralUtility::makeInstance($className);
4461
                        if (method_exists($hookObj, 'processTranslateTo_copyAction')) {
4462
                            $hookObj->processTranslateTo_copyAction($row[$fN], $langRec, $this, $fN);
4463
                        }
4464
                    }
4465
                    if (!empty($translateToMsg)) {
4466
                        $overrideValues[$fN] = '[' . $translateToMsg . '] ' . $row[$fN];
4467
                    } else {
4468
                        $overrideValues[$fN] = $row[$fN];
4469
                    }
4470
                }
4471
            }
4472
        }
4473
4474
        if ($table !== 'pages') {
4475
            // Get the uid of record after which this localized record should be inserted
4476
            $previousUid = $this->getPreviousLocalizedRecordUid($table, $uid, $row['pid'], $language);
4477
            // Execute the copy:
4478
            $newId = $this->copyRecord($table, $uid, -$previousUid, true, $overrideValues, '', $language);
4479
            $autoVersionNewId = $this->getAutoVersionId($table, $newId);
4480
            if ($autoVersionNewId !== null) {
4481
                $this->triggerRemapAction($table, $newId, [$this, 'placeholderShadowing'], [$table, $autoVersionNewId], true);
4482
            }
4483
        } else {
4484
            // Create new page which needs to contain the same pid as the original page
4485
            $overrideValues['pid'] = $row['pid'];
4486
            // Take over the hidden state of the original language state, this is done due to legacy reasons where-as
4487
            // pages_language_overlay was set to "hidden -> default=0" but pages hidden -> default 1"
4488
            if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
4489
                $hiddenFieldName = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
4490
                $overrideValues[$hiddenFieldName] = $row[$hiddenFieldName] ?? $GLOBALS['TCA'][$table]['columns'][$hiddenFieldName]['config']['default'];
4491
            }
4492
            $temporaryId = StringUtility::getUniqueId('NEW');
4493
            $copyTCE = $this->getLocalTCE();
4494
            $copyTCE->start([$table => [$temporaryId => $overrideValues]], [], $this->BE_USER);
4495
            $copyTCE->process_datamap();
4496
            // Getting the new UID as if it had been copied:
4497
            $theNewSQLID = $copyTCE->substNEWwithIDs[$temporaryId];
4498
            if ($theNewSQLID) {
4499
                $this->copyMappingArray[$table][$uid] = $theNewSQLID;
4500
                $newId = $theNewSQLID;
4501
            }
4502
        }
4503
4504
        return $newId;
4505
    }
4506
4507
    /**
4508
     * Performs localization or synchronization of child records.
4509
     * The $command argument expects an array, but supports a string for backward-compatibility.
4510
     *
4511
     * $command = array(
4512
     *   'field' => 'tx_myfieldname',
4513
     *   'language' => 2,
4514
     *   // either the key 'action' or 'ids' must be set
4515
     *   'action' => 'synchronize', // or 'localize'
4516
     *   'ids' => array(1, 2, 3, 4) // child element ids
4517
     * );
4518
     *
4519
     * @param string $table The table of the localized parent record
4520
     * @param int $id The uid of the localized parent record
4521
     * @param array|string $command Defines the command to be performed (see example above)
4522
     */
4523
    protected function inlineLocalizeSynchronize($table, $id, $command)
4524
    {
4525
        $parentRecord = BackendUtility::getRecordWSOL($table, $id);
4526
4527
        // Backward-compatibility handling
4528
        if (!is_array($command)) {
4529
            // <field>, (localize | synchronize | <uid>):
4530
            $parts = GeneralUtility::trimExplode(',', $command);
4531
            $command = [
4532
                'field' => $parts[0],
4533
                // The previous process expected $id to point to the localized record already
4534
                'language' => (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]
4535
            ];
4536
            if (!MathUtility::canBeInterpretedAsInteger($parts[1])) {
4537
                $command['action'] = $parts[1];
4538
            } else {
4539
                $command['ids'] = [$parts[1]];
4540
            }
4541
        }
4542
4543
        // In case the parent record is the default language record, fetch the localization
4544
        if (empty($parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
4545
            // Fetch the live record
4546
            // @todo: this needs to be revisited, as getRecordLocalization() does a BackendWorkspaceRestriction
4547
            // based on $GLOBALS[BE_USER], which could differ from the $this->BE_USER->workspace value
4548
            $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND t3ver_oid=0');
4549
            if (empty($parentRecordLocalization)) {
4550
                if ($this->enableLogging) {
4551
                    $this->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Localization for parent record ' . $table . ':' . $id . '" cannot be fetched', -1, [], $this->eventPid($table, $id, $parentRecord['pid']));
4552
                }
4553
                return;
4554
            }
4555
            $parentRecord = $parentRecordLocalization[0];
4556
            $id = $parentRecord['uid'];
4557
            // Process overlay for current selected workspace
4558
            BackendUtility::workspaceOL($table, $parentRecord);
4559
        }
4560
4561
        $field = $command['field'];
4562
        $language = $command['language'];
4563
        $action = $command['action'];
4564
        $ids = $command['ids'];
4565
4566
        if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4567
            return;
4568
        }
4569
4570
        $config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4571
        $foreignTable = $config['foreign_table'];
4572
4573
        $transOrigPointer = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
4574
        $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
4575
4576
        if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) {
4577
            return;
4578
        }
4579
4580
        $inlineSubType = $this->getInlineFieldType($config);
4581
        if ($inlineSubType === false) {
4582
            return;
4583
        }
4584
4585
        $transOrigRecord = BackendUtility::getRecordWSOL($table, $transOrigPointer);
4586
4587
        $removeArray = [];
4588
        $mmTable = $inlineSubType === 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : '';
4589
        // Fetch children from original language parent:
4590
        /** @var RelationHandler $dbAnalysisOriginal */
4591
        $dbAnalysisOriginal = $this->createRelationHandlerInstance();
4592
        $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $table, $config);
4593
        $elementsOriginal = [];
4594
        foreach ($dbAnalysisOriginal->itemArray as $item) {
4595
            $elementsOriginal[$item['id']] = $item;
4596
        }
4597
        unset($dbAnalysisOriginal);
4598
        // Fetch children from current localized parent:
4599
        /** @var RelationHandler $dbAnalysisCurrent */
4600
        $dbAnalysisCurrent = $this->createRelationHandlerInstance();
4601
        $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
4602
        // Perform synchronization: Possibly removal of already localized records:
4603
        if ($action === 'synchronize') {
4604
            foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
4605
                $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
4606
                if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
4607
                    $childTransOrigPointer = $childRecord[$childTransOrigPointerField];
4608
                    // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it:
4609
                    if (!isset($elementsOriginal[$childTransOrigPointer])) {
4610
                        unset($dbAnalysisCurrent->itemArray[$index]);
4611
                        $removeArray[$item['table']][$item['id']]['delete'] = 1;
4612
                    }
4613
                }
4614
            }
4615
        }
4616
        // Perform synchronization/localization: Possibly add unlocalized records for original language:
4617
        if ($action === 'localize' || $action === 'synchronize') {
4618
            foreach ($elementsOriginal as $originalId => $item) {
4619
                $item['id'] = $this->localize($item['table'], $item['id'], $language);
4620
                $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4621
                $dbAnalysisCurrent->itemArray[] = $item;
4622
            }
4623
        } elseif (!empty($ids)) {
4624
            foreach ($ids as $childId) {
4625
                if (!MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) {
4626
                    continue;
4627
                }
4628
                $item = $elementsOriginal[$childId];
4629
                $item['id'] = $this->localize($item['table'], $item['id'], $language);
4630
                $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4631
                $dbAnalysisCurrent->itemArray[] = $item;
4632
            }
4633
        }
4634
        // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4635
        $value = implode(',', $dbAnalysisCurrent->getValueArray());
4636
        $this->registerDBList[$table][$id][$field] = $value;
4637
        // Remove child records (if synchronization requested it):
4638
        if (is_array($removeArray) && !empty($removeArray)) {
4639
            /** @var DataHandler $tce */
4640
            $tce = GeneralUtility::makeInstance(__CLASS__, $this->referenceIndexUpdater);
4641
            $tce->enableLogging = $this->enableLogging;
4642
            $tce->start([], $removeArray, $this->BE_USER);
4643
            $tce->process_cmdmap();
4644
            unset($tce);
4645
        }
4646
        $updateFields = [];
4647
        // Handle, reorder and store relations:
4648
        if ($inlineSubType === 'list') {
4649
            $updateFields = [$field => $value];
4650
        } elseif ($inlineSubType === 'field') {
4651
            $dbAnalysisCurrent->writeForeignField($config, $id);
4652
            $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4653
        } elseif ($inlineSubType === 'mm') {
4654
            $dbAnalysisCurrent->writeMM($config['MM'], $id);
4655
            $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4656
        }
4657
        // Update field referencing to child records of localized parent record:
4658
        if (!empty($updateFields)) {
4659
            $this->updateDB($table, $id, $updateFields);
4660
        }
4661
    }
4662
4663
    /*********************************************
4664
     *
4665
     * Cmd: Deleting
4666
     *
4667
     ********************************************/
4668
    /**
4669
     * Delete a single record
4670
     *
4671
     * @param string $table Table name
4672
     * @param int $id Record UID
4673
     * @internal should only be used from within DataHandler
4674
     */
4675
    public function deleteAction($table, $id)
4676
    {
4677
        $recordToDelete = BackendUtility::getRecord($table, $id);
4678
        // Record asked to be deleted was found:
4679
        if (is_array($recordToDelete)) {
4680
            $recordWasDeleted = false;
4681
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
4682
                $hookObj = GeneralUtility::makeInstance($className);
4683
                if (method_exists($hookObj, 'processCmdmap_deleteAction')) {
4684
                    $hookObj->processCmdmap_deleteAction($table, $id, $recordToDelete, $recordWasDeleted, $this);
4685
                }
4686
            }
4687
            // Delete the record if a hook hasn't deleted it yet
4688
            if (!$recordWasDeleted) {
0 ignored issues
show
introduced by
The condition $recordWasDeleted is always false.
Loading history...
4689
                $this->deleteEl($table, $id);
4690
            }
4691
        }
4692
    }
4693
4694
    /**
4695
     * Delete element from any table
4696
     *
4697
     * @param string $table Table name
4698
     * @param int $uid Record UID
4699
     * @param bool $noRecordCheck Flag: If $noRecordCheck is set, then the function does not check permission to delete record
4700
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4701
     * @param bool $deleteRecordsOnPage If false and if deleting pages, records on the page will not be deleted (edge case while swapping workspaces)
4702
     * @internal should only be used from within DataHandler
4703
     */
4704
    public function deleteEl($table, $uid, $noRecordCheck = false, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4705
    {
4706
        if ($table === 'pages') {
4707
            $this->deletePages($uid, $noRecordCheck, $forceHardDelete, $deleteRecordsOnPage);
4708
        } else {
4709
            $this->deleteVersionsForRecord($table, $uid, $forceHardDelete);
4710
            $this->deleteRecord($table, $uid, $noRecordCheck, $forceHardDelete);
4711
        }
4712
    }
4713
4714
    /**
4715
     * Delete versions for element from any table
4716
     *
4717
     * @param string $table Table name
4718
     * @param int $uid Record UID
4719
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4720
     * @internal should only be used from within DataHandler
4721
     */
4722
    public function deleteVersionsForRecord($table, $uid, $forceHardDelete)
4723
    {
4724
        $versions = BackendUtility::selectVersionsOfRecord($table, $uid, 'uid,pid,t3ver_wsid,t3ver_state', $this->BE_USER->workspace ?: null);
4725
        if (is_array($versions)) {
4726
            foreach ($versions as $verRec) {
4727
                if (!$verRec['_CURRENT_VERSION']) {
4728
                    $currentUserWorkspace = null;
4729
                    if ((int)$verRec['t3ver_wsid'] !== (int)$this->BE_USER->workspace) {
4730
                        // If deleting records from 'foreign' / 'other' workspaces, the be user must be put into
4731
                        // this workspace temporarily so stuff like refindex updating is registered for this workspace
4732
                        // when deleting records in there.
4733
                        $currentUserWorkspace = $this->BE_USER->workspace;
4734
                        $this->BE_USER->workspace = (int)$verRec['t3ver_wsid'];
4735
                    }
4736
                    if ($table === 'pages') {
4737
                        $this->deletePages($verRec['uid'], true, $forceHardDelete);
4738
                    } else {
4739
                        $this->deleteRecord($table, $verRec['uid'], true, $forceHardDelete);
4740
                    }
4741
                    if ($currentUserWorkspace !== null) {
4742
                        // Switch back workspace
4743
                        $this->BE_USER->workspace = $currentUserWorkspace;
4744
                    }
4745
                }
4746
            }
4747
        }
4748
    }
4749
4750
    /**
4751
     * Undelete a single record
4752
     *
4753
     * @param string $table Table name
4754
     * @param int $uid Record UID
4755
     * @internal should only be used from within DataHandler
4756
     */
4757
    public function undeleteRecord($table, $uid)
4758
    {
4759
        if ($this->isRecordUndeletable($table, $uid)) {
4760
            $this->deleteRecord($table, $uid, true, false, true);
4761
        }
4762
    }
4763
4764
    /**
4765
     * Deleting/Undeleting a record
4766
     * This function may not be used to delete pages-records unless the underlying records are already deleted
4767
     * Deletes a record regardless of versioning state (live or offline, doesn't matter, the uid decides)
4768
     * If both $noRecordCheck and $forceHardDelete are set it could even delete a "deleted"-flagged record!
4769
     *
4770
     * @param string $table Table name
4771
     * @param int $uid Record UID
4772
     * @param bool $noRecordCheck Flag: If $noRecordCheck is set, then the function does not check permission to delete record
4773
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4774
     * @param bool $undeleteRecord If TRUE, the "deleted" flag is set to 0 again and thus, the item is undeleted.
4775
     * @internal should only be used from within DataHandler
4776
     */
4777
    public function deleteRecord($table, $uid, $noRecordCheck = false, $forceHardDelete = false, $undeleteRecord = false)
4778
    {
4779
        $currentUserWorkspace = (int)$this->BE_USER->workspace;
4780
        $uid = (int)$uid;
4781
        if (!$GLOBALS['TCA'][$table] || !$uid) {
4782
            $this->log($table, $uid, SystemLogDatabaseAction::DELETE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to delete record without delete-permissions. [' . $this->BE_USER->errorMsg . ']');
4783
            return;
4784
        }
4785
        // Skip processing already deleted records
4786
        if (!$forceHardDelete && !$undeleteRecord && $this->hasDeletedRecord($table, $uid)) {
4787
            return;
4788
        }
4789
4790
        // Checking if there is anything else disallowing deleting the record by checking if editing is allowed
4791
        $deletedRecord = $forceHardDelete || $undeleteRecord;
4792
        $fullLanguageAccessCheck = true;
4793
        if ($table === 'pages') {
4794
            // If this is a page translation, the full language access check should not be done
4795
            $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
4796
            if ($defaultLanguagePageId !== $uid) {
4797
                $fullLanguageAccessCheck = false;
4798
            }
4799
        }
4800
        $hasEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, $deletedRecord, $fullLanguageAccessCheck);
4801
        if (!$hasEditAccess) {
4802
            $this->log($table, $uid, SystemLogDatabaseAction::DELETE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to delete record without delete-permissions');
4803
            return;
4804
        }
4805
        if ($table === 'pages') {
4806
            $perms = Permission::PAGE_DELETE;
4807
        } elseif ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
4808
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
4809
            $perms = Permission::PAGE_EDIT;
4810
        } else {
4811
            $perms = Permission::CONTENT_EDIT;
4812
        }
4813
        if (!$noRecordCheck && !$this->doesRecordExist($table, $uid, $perms)) {
4814
            return;
4815
        }
4816
4817
        // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
4818
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4818
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4819
        $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4820
        $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
4821
        $databaseErrorMessage = '';
4822
        if ($deleteField && !$forceHardDelete) {
4823
            $updateFields = [
4824
                $deleteField => $undeleteRecord ? 0 : 1
4825
            ];
4826
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4827
                $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4828
            }
4829
            // before (un-)deleting this record, check for child records or references
4830
            $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
4831
            try {
4832
                // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
4833
                if (!$undeleteRecord) {
4834
                    $this->deletedRecords[$table][] = (int)$uid;
4835
                    $this->deleteL10nOverlayRecords($table, $uid);
4836
                }
4837
                GeneralUtility::makeInstance(ConnectionPool::class)
4838
                    ->getConnectionForTable($table)
4839
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4840
            } catch (DBALException $e) {
4841
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4842
            }
4843
        } else {
4844
            // Delete the hard way...:
4845
            try {
4846
                $this->hardDeleteSingleRecord($table, (int)$uid);
4847
                $this->deletedRecords[$table][] = (int)$uid;
4848
                $this->deleteL10nOverlayRecords($table, $uid);
4849
            } catch (DBALException $e) {
4850
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4851
            }
4852
        }
4853
        if ($this->enableLogging) {
4854
            $state = $undeleteRecord ? SystemLogDatabaseAction::INSERT : SystemLogDatabaseAction::DELETE;
4855
            if ($databaseErrorMessage === '') {
4856
                if ($forceHardDelete) {
4857
                    $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
4858
                } else {
4859
                    $message = $state === 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
4860
                }
4861
                $propArr = $this->getRecordProperties($table, $uid);
4862
                $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4863
4864
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::MESSAGE, $message, 0, [
4865
                    $propArr['header'],
4866
                    $table . ':' . $uid,
4867
                    $pagePropArr['header'],
4868
                    $propArr['pid']
4869
                ], $propArr['event_pid']);
4870
            } else {
4871
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::TODAYS_SPECIAL, $databaseErrorMessage);
4872
            }
4873
        }
4874
4875
        // Add history entry
4876
        if ($undeleteRecord) {
4877
            $this->getRecordHistoryStore()->undeleteRecord($table, $uid, $this->correlationId);
4878
        } else {
4879
            $this->getRecordHistoryStore()->deleteRecord($table, $uid, $this->correlationId);
4880
        }
4881
4882
        // Update reference index with table/uid on left side (recuid)
4883
        $this->updateRefIndex($table, $uid);
4884
        // Update reference index with table/uid on right side (ref_uid). Important if children of a relation are deleted / undeleted.
4885
        $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, $uid, $currentUserWorkspace);
4886
    }
4887
4888
    /**
4889
     * Used to delete page because it will check for branch below pages and disallowed tables on the page as well.
4890
     *
4891
     * @param int $uid Page id
4892
     * @param bool $force If TRUE, pages are not checked for permission.
4893
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4894
     * @param bool $deleteRecordsOnPage If false, records on the page will not be deleted (edge case while swapping workspaces)
4895
     * @internal should only be used from within DataHandler
4896
     */
4897
    public function deletePages($uid, $force = false, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4898
    {
4899
        $uid = (int)$uid;
4900
        if ($uid === 0) {
4901
            if ($this->enableLogging) {
4902
                $this->log('pages', $uid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'Deleting all pages starting from the root-page is disabled.', -1, [], 0);
4903
            }
4904
            return;
4905
        }
4906
        // Getting list of pages to delete:
4907
        if ($force) {
4908
            // Returns the branch WITHOUT permission checks (0 secures that), so it cannot return -1
4909
            $pageIdsInBranch = $this->doesBranchExist('', $uid, 0, true);
4910
            $res = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
4911
        } else {
4912
            $res = $this->canDeletePage($uid);
4913
        }
4914
        // Perform deletion if not error:
4915
        if (is_array($res)) {
4916
            foreach ($res as $deleteId) {
4917
                $this->deleteSpecificPage($deleteId, $forceHardDelete, $deleteRecordsOnPage);
4918
            }
4919
        } else {
4920
            /** @var FlashMessage $flashMessage */
4921
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
4922
            /** @var FlashMessageService $flashMessageService */
4923
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
4924
            $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
4925
            $this->newlog($res, SystemLogErrorClassification::USER_ERROR);
4926
        }
4927
    }
4928
4929
    /**
4930
     * Delete a page (or set deleted field to 1) and all records on it.
4931
     *
4932
     * @param int $uid Page id
4933
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4934
     * @param bool $deleteRecordsOnPage If false, records on the page will not be deleted (edge case while swapping workspaces)
4935
     * @internal
4936
     * @see deletePages()
4937
     */
4938
    public function deleteSpecificPage($uid, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4939
    {
4940
        $uid = (int)$uid;
4941
        if (!$uid) {
4942
            // Early void return on invalid uid
4943
            return;
4944
        }
4945
        $forceHardDelete = (bool)$forceHardDelete;
4946
4947
        // Delete either a default language page or a translated page
4948
        $pageIdInDefaultLanguage = $this->getDefaultLanguagePageId($uid);
4949
        $isPageTranslation = false;
4950
        $pageLanguageId = 0;
4951
        if ($pageIdInDefaultLanguage !== $uid) {
4952
            // For translated pages, translated records in other tables (eg. tt_content) for the
4953
            // to-delete translated page have their pid field set to the uid of the default language record,
4954
            // NOT the uid of the translated page record.
4955
            // If a translated page is deleted, only translations of records in other tables of this language
4956
            // should be deleted. The code checks if the to-delete page is a translated page and
4957
            // adapts the query for other tables to use the uid of the default language page as pid together
4958
            // with the language id of the translated page.
4959
            $isPageTranslation = true;
4960
            $pageLanguageId = $this->pageInfo($uid, $GLOBALS['TCA']['pages']['ctrl']['languageField']);
4961
        }
4962
4963
        if ($deleteRecordsOnPage) {
4964
            $tableNames = $this->compileAdminTables();
4965
            foreach ($tableNames as $table) {
4966
                if ($table === 'pages' || ($isPageTranslation && !BackendUtility::isTableLocalizable($table))) {
4967
                    // Skip pages table. And skip table if not translatable, but a translated page is deleted
4968
                    continue;
4969
                }
4970
4971
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4972
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
4973
                $queryBuilder
4974
                    ->select('uid')
4975
                    ->from($table);
4976
4977
                if ($isPageTranslation) {
4978
                    // Only delete records in the specified language
4979
                    $queryBuilder->where(
4980
                        $queryBuilder->expr()->eq(
4981
                            'pid',
4982
                            $queryBuilder->createNamedParameter($pageIdInDefaultLanguage, \PDO::PARAM_INT)
4983
                        ),
4984
                        $queryBuilder->expr()->eq(
4985
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
4986
                            $queryBuilder->createNamedParameter($pageLanguageId, \PDO::PARAM_INT)
4987
                        )
4988
                    );
4989
                } else {
4990
                    // Delete all records on this page
4991
                    $queryBuilder->where(
4992
                        $queryBuilder->expr()->eq(
4993
                            'pid',
4994
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
4995
                        )
4996
                    );
4997
                }
4998
4999
                $currentUserWorkspace = (int)$this->BE_USER->workspace;
5000
                if ($currentUserWorkspace !== 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5001
                    // If we are in a workspace, make sure only records of this workspace are deleted.
5002
                    $queryBuilder->andWhere(
5003
                        $queryBuilder->expr()->eq(
5004
                            't3ver_wsid',
5005
                            $queryBuilder->createNamedParameter($currentUserWorkspace, \PDO::PARAM_INT)
5006
                        )
5007
                    );
5008
                }
5009
5010
                $statement = $queryBuilder->execute();
5011
5012
                while ($row = $statement->fetch()) {
5013
                    // Delete any further workspace overlays of the record in question, then delete the record.
5014
                    $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
5015
                    $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
5016
                }
5017
            }
5018
        }
5019
5020
        // Delete any further workspace overlays of the record in question, then delete the record.
5021
        $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
5022
        $this->deleteRecord('pages', $uid, true, $forceHardDelete);
5023
    }
5024
5025
    /**
5026
     * Used to evaluate if a page can be deleted
5027
     *
5028
     * @param int $uid Page id
5029
     * @return int[]|string If array: List of page uids to traverse and delete (means OK), if string: error message.
5030
     * @internal should only be used from within DataHandler
5031
     */
5032
    public function canDeletePage($uid)
5033
    {
5034
        $uid = (int)$uid;
5035
        $isTranslatedPage = null;
5036
5037
        // If we may at all delete this page
5038
        // If this is a page translation, do the check against the perms_* of the default page
5039
        // Because it is currently only deleting the translation
5040
        $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
5041
        if ($defaultLanguagePageId !== $uid) {
5042
            if ($this->doesRecordExist('pages', (int)$defaultLanguagePageId, Permission::PAGE_DELETE)) {
5043
                $isTranslatedPage = true;
5044
            } else {
5045
                return 'Attempt to delete page without permissions';
5046
            }
5047
        } elseif (!$this->doesRecordExist('pages', $uid, Permission::PAGE_DELETE)) {
5048
            return 'Attempt to delete page without permissions';
5049
        }
5050
5051
        $pageIdsInBranch = $this->doesBranchExist('', $uid, Permission::PAGE_DELETE, true);
5052
5053
        if ($pageIdsInBranch === -1) {
5054
            return 'Attempt to delete pages in branch without permissions';
5055
        }
5056
5057
        $pagesInBranch = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
5058
5059
        if ($disallowedTables = $this->checkForRecordsFromDisallowedTables($pagesInBranch)) {
5060
            return 'Attempt to delete records from disallowed tables (' . implode(', ', $disallowedTables) . ')';
5061
        }
5062
5063
        foreach ($pagesInBranch as $pageInBranch) {
5064
            if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, $isTranslatedPage ? false : true)) {
5065
                return 'Attempt to delete page which has prohibited localizations.';
5066
            }
5067
        }
5068
        return $pagesInBranch;
5069
    }
5070
5071
    /**
5072
     * Returns TRUE if record CANNOT be deleted, otherwise FALSE. Used to check before the versioning API allows a record to be marked for deletion.
5073
     *
5074
     * @param string $table Record Table
5075
     * @param int $id Record UID
5076
     * @return string Returns a string IF there is an error (error string explaining). FALSE means record can be deleted
5077
     * @internal should only be used from within DataHandler
5078
     */
5079
    public function cannotDeleteRecord($table, $id)
5080
    {
5081
        if ($table === 'pages') {
5082
            $res = $this->canDeletePage($id);
5083
            return is_array($res) ? false : $res;
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_array($res) ? false : $res could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
5084
        }
5085
        if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
5086
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
5087
            $perms = Permission::PAGE_EDIT;
5088
        } else {
5089
            $perms = Permission::CONTENT_EDIT;
5090
        }
5091
        return $this->doesRecordExist($table, $id, $perms) ? false : 'No permission to delete record';
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->doesRecord...ssion to delete record' could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
5092
    }
5093
5094
    /**
5095
     * Determines whether a record can be undeleted.
5096
     *
5097
     * @param string $table Table name of the record
5098
     * @param int $uid uid of the record
5099
     * @return bool Whether the record can be undeleted
5100
     * @internal should only be used from within DataHandler
5101
     */
5102
    public function isRecordUndeletable($table, $uid)
5103
    {
5104
        $result = false;
5105
        $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5106
        if ($record['pid']) {
5107
            $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5108
            // The page containing the record is not deleted, thus the record can be undeleted:
5109
            if (!$page['deleted']) {
5110
                $result = true;
5111
            } else {
5112
                $this->log($table, $uid, SystemLogDatabaseAction::DELETE, '', SystemLogErrorClassification::USER_ERROR, 'Record cannot be undeleted since the page containing it is deleted! Undelete page "' . $page['title'] . ' (UID: ' . $page['uid'] . ')" first');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $recpid of TYPO3\CMS\Core\DataHandling\DataHandler::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5112
                $this->log($table, $uid, SystemLogDatabaseAction::DELETE, /** @scrutinizer ignore-type */ '', SystemLogErrorClassification::USER_ERROR, 'Record cannot be undeleted since the page containing it is deleted! Undelete page "' . $page['title'] . ' (UID: ' . $page['uid'] . ')" first');
Loading history...
5113
            }
5114
        } else {
5115
            // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5116
            $result = true;
5117
        }
5118
        return $result;
5119
    }
5120
5121
    /**
5122
     * Before a record is deleted, check if it has references such as inline type or MM references.
5123
     * If so, set these child records also to be deleted.
5124
     *
5125
     * @param string $table Record Table
5126
     * @param string $uid Record UID
5127
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5128
     * @see deleteRecord()
5129
     * @internal should only be used from within DataHandler
5130
     */
5131
    public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
5132
    {
5133
        $conf = $GLOBALS['TCA'][$table]['columns'];
5134
        $row = BackendUtility::getRecord($table, $uid, '*', '', false);
0 ignored issues
show
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Utilit...endUtility::getRecord(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5134
        $row = BackendUtility::getRecord($table, /** @scrutinizer ignore-type */ $uid, '*', '', false);
Loading history...
5135
        if (empty($row)) {
5136
            return;
5137
        }
5138
        foreach ($row as $field => $value) {
5139
            $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5140
        }
5141
    }
5142
5143
    /**
5144
     * Process fields of a record to be deleted and search for special handling, like
5145
     * inline type, MM records, etc.
5146
     *
5147
     * @param string $table Record Table
5148
     * @param string $uid Record UID
5149
     * @param string $field Record field
5150
     * @param string $value Record field value
5151
     * @param array $conf TCA configuration of current field
5152
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5153
     * @see deleteRecord()
5154
     * @internal should only be used from within DataHandler
5155
     */
5156
    public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
5157
    {
5158
        if ($conf['type'] === 'inline') {
5159
            $foreign_table = $conf['foreign_table'];
5160
            if ($foreign_table) {
5161
                $inlineType = $this->getInlineFieldType($conf);
5162
                if ($inlineType === 'list' || $inlineType === 'field') {
5163
                    /** @var RelationHandler $dbAnalysis */
5164
                    $dbAnalysis = $this->createRelationHandlerInstance();
5165
                    $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
0 ignored issues
show
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $MMuid of TYPO3\CMS\Core\Database\RelationHandler::start(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5165
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
5166
                    $dbAnalysis->undeleteRecord = true;
5167
5168
                    $enableCascadingDelete = true;
5169
                    // non type save comparison is intended!
5170
                    if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5171
                        $enableCascadingDelete = false;
5172
                    }
5173
5174
                    // Walk through the items and remove them
5175
                    foreach ($dbAnalysis->itemArray as $v) {
5176
                        if (!$undeleteRecord) {
5177
                            if ($enableCascadingDelete) {
5178
                                $this->deleteAction($v['table'], $v['id']);
5179
                            }
5180
                        } else {
5181
                            $this->undeleteRecord($v['table'], $v['id']);
5182
                        }
5183
                    }
5184
                }
5185
            }
5186
        } elseif ($this->isReferenceField($conf)) {
5187
            $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5188
            $dbAnalysis = $this->createRelationHandlerInstance();
5189
            $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5190
            foreach ($dbAnalysis->itemArray as $v) {
5191
                $this->updateRefIndex($v['table'], $v['id']);
5192
            }
5193
        }
5194
    }
5195
5196
    /**
5197
     * Find l10n-overlay records and perform the requested delete action for these records.
5198
     *
5199
     * @param string $table Record Table
5200
     * @param string $uid Record UID
5201
     * @internal should only be used from within DataHandler
5202
     */
5203
    public function deleteL10nOverlayRecords($table, $uid)
5204
    {
5205
        // Check whether table can be localized
5206
        if (!BackendUtility::isTableLocalizable($table)) {
5207
            return;
5208
        }
5209
5210
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5211
        $queryBuilder->getRestrictions()
5212
            ->removeAll()
5213
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5214
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
5215
5216
        $queryBuilder->select('*')
5217
            ->from($table)
5218
            ->where(
5219
                $queryBuilder->expr()->eq(
5220
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5221
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5222
                )
5223
            );
5224
5225
        $result = $queryBuilder->execute();
5226
        while ($record = $result->fetch()) {
5227
            // Ignore workspace delete placeholders. Those records have been marked for
5228
            // deletion before - deleting them again in a workspace would revert that state.
5229
            if ((int)$this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5230
                BackendUtility::workspaceOL($table, $record, $this->BE_USER->workspace);
5231
                if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5232
                    continue;
5233
                }
5234
            }
5235
            $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5236
        }
5237
    }
5238
5239
    /*********************************************
5240
     *
5241
     * Cmd: Workspace discard & flush
5242
     *
5243
     ********************************************/
5244
5245
    /**
5246
     * Discard a versioned record from this workspace. This deletes records from the database - no soft delete.
5247
     * This main entry method is called recursive for sub pages, localizations, relations and records on a page.
5248
     * The method checks user access and gathers facts about this record to hand the deletion over to detail methods.
5249
     *
5250
     * The incoming $uid or $row can be anything: The workspace of current user is respected and only records
5251
     * of current user workspace are discarded. If giving a live record uid, the versioned overly will be fetched.
5252
     *
5253
     * @param string $table Database table name
5254
     * @param int|null $uid Uid of live or versioned record to be discarded, or null if $record is given
5255
     * @param array|null $record Record row that should be discarded. Used instead of $uid within recursion.
5256
     */
5257
    public function discard(string $table, ?int $uid, array $record = null): void
5258
    {
5259
        if ($uid === null && $record === null) {
5260
            throw new \RuntimeException('Either record $uid or $record row must be given', 1600373491);
5261
        }
5262
5263
        // Fetch record we are dealing with if not given
5264
        if ($record === null) {
5265
            $record = BackendUtility::getRecord($table, $uid);
5266
        }
5267
        if (!is_array($record)) {
5268
            return;
5269
        }
5270
        $uid = (int)$record['uid'];
5271
5272
        // Call hook and return if hook took care of the element
5273
        $recordWasDiscarded = false;
5274
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
5275
            $hookObj = GeneralUtility::makeInstance($className);
5276
            if (method_exists($hookObj, 'processCmdmap_discardAction')) {
5277
                $hookObj->processCmdmap_discardAction($table, $uid, $record, $recordWasDiscarded);
5278
            }
5279
        }
5280
5281
        $userWorkspace = (int)$this->BE_USER->workspace;
5282
        if ($recordWasDiscarded
5283
            || $userWorkspace === 0
5284
            || !BackendUtility::isTableWorkspaceEnabled($table)
5285
            || $this->hasDeletedRecord($table, $uid)
5286
        ) {
5287
            return;
5288
        }
5289
5290
        // Gather versioned and placeholder record if there are any
5291
        $versionRecord = null;
5292
        $placeholderRecord = null;
5293
        if ((int)$record['t3ver_wsid'] === 0) {
5294
            $record = BackendUtility::getWorkspaceVersionOfRecord($userWorkspace, $table, $uid);
5295
        }
5296
        if (!is_array($record)) {
5297
            return;
5298
        }
5299
        $recordState = VersionState::cast($record['t3ver_state']);
5300
        if ($recordState->equals(VersionState::NEW_PLACEHOLDER)) {
5301
            $placeholderRecord = $record;
5302
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($userWorkspace, $table, $uid);
5303
            if (!is_array($versionRecord)) {
5304
                return;
5305
            }
5306
        } elseif ($recordState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
5307
            $versionRecord = $record;
5308
            $placeholderRecord = BackendUtility::getLiveVersionOfRecord($table, $uid);
5309
            if (!is_array($placeholderRecord)) {
5310
                return;
5311
            }
5312
        } else {
5313
            $versionRecord = $record;
5314
        }
5315
        // Do not use $record, $recordState and $uid below anymore, rely on $versionRecord and $placeholderRecord
5316
5317
        // User access checks
5318
        if ($userWorkspace !== (int)$versionRecord['t3ver_wsid']) {
5319
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: Different workspace', SystemLogErrorClassification::USER_ERROR);
5320
            return;
5321
        }
5322
        if ($errorCode = $this->BE_USER->workspaceCannotEditOfflineVersion($table, $versionRecord['uid'])) {
5323
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR);
5324
            return;
5325
        }
5326
        if (!$this->checkRecordUpdateAccess($table, $versionRecord['uid'])) {
5327
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: User has no edit access', SystemLogErrorClassification::USER_ERROR);
5328
            return;
5329
        }
5330
        $fullLanguageAccessCheck = !($table === 'pages' && (int)$versionRecord[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] !== 0);
5331
        if (!$this->BE_USER->recordEditAccessInternals($table, $versionRecord, false, true, $fullLanguageAccessCheck)) {
5332
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: User has no delete access', SystemLogErrorClassification::USER_ERROR);
5333
            return;
5334
        }
5335
5336
        // Perform discard operations
5337
        $versionState = VersionState::cast($versionRecord['t3ver_state']);
5338
        if ($table === 'pages' && $versionState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
5339
            // When discarding a new page page, there can be new sub pages and new records.
5340
            // Those need to be discarded, otherwise they'd end up as records without parent page.
5341
            $this->discardSubPagesAndRecordsOnPage($versionRecord);
5342
        }
5343
5344
        $this->discardLocalizationOverlayRecords($table, $versionRecord);
5345
        $this->discardRecordRelations($table, $versionRecord);
5346
        $this->hardDeleteSingleRecord($table, (int)$versionRecord['uid']);
5347
        $this->deletedRecords[$table][] = (int)$versionRecord['uid'];
5348
        $this->registerReferenceIndexRowsForDrop($table, (int)$versionRecord['uid'], $userWorkspace);
5349
        $this->getRecordHistoryStore()->deleteRecord($table, (int)$versionRecord['uid'], $this->correlationId);
5350
        $this->log(
5351
            $table,
5352
            (int)$versionRecord['uid'],
5353
            SystemLogDatabaseAction::DELETE,
5354
            0,
5355
            SystemLogErrorClassification::MESSAGE,
5356
            'Record ' . $table . ':' . $versionRecord['uid'] . ' was deleted unrecoverable from page ' . $versionRecord['pid'],
5357
            0,
5358
            [],
5359
            (int)$versionRecord['pid']
5360
        );
5361
5362
        if ($versionState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
5363
            // Drop placeholder records if any
5364
            $this->hardDeleteSingleRecord($table, (int)$placeholderRecord['uid']);
5365
            $this->deletedRecords[$table][] = (int)$placeholderRecord['uid'];
5366
        }
5367
    }
5368
5369
    /**
5370
     * Also discard any sub pages and records of a new parent page if this page is discarded.
5371
     * Discarding only in specific localization, if needed.
5372
     *
5373
     * @param array $page Page record row
5374
     */
5375
    protected function discardSubPagesAndRecordsOnPage(array $page): void
5376
    {
5377
        $isLocalizedPage = false;
5378
        $sysLanguageId = (int)$page[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
5379
        if ($sysLanguageId > 0) {
5380
            // New or moved localized page.
5381
            // Discard records on this page localization, but no sub pages.
5382
            // Records of a translated page have the pid set to the default language page uid. Found in l10n_parent.
5383
            // @todo: Discard other page translations that inherit from this?! (l10n_source field)
5384
            $isLocalizedPage = true;
5385
            $pid = (int)$page[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
5386
        } else {
5387
            // New or moved default language page.
5388
            // Discard any sub pages and all other records of this page, including any page localizations.
5389
            // The t3ver_state=-1 record is incoming here. Records on this page have their pid field set to the uid
5390
            // of the t3ver_state=1 record, which is in the t3ver_oid field of the incoming record.
5391
            $pid = (int)$page['t3ver_oid'];
5392
        }
5393
        $tables = $this->compileAdminTables();
5394
        foreach ($tables as $table) {
5395
            if (($isLocalizedPage && $table === 'pages')
5396
                || ($isLocalizedPage && !BackendUtility::isTableLocalizable($table))
5397
                || !BackendUtility::isTableWorkspaceEnabled($table)
5398
            ) {
5399
                continue;
5400
            }
5401
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5402
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5403
            $queryBuilder->select('*')
5404
                ->from($table)
5405
                ->where(
5406
                    $queryBuilder->expr()->eq(
5407
                        'pid',
5408
                        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
5409
                    ),
5410
                    $queryBuilder->expr()->eq(
5411
                        't3ver_wsid',
5412
                        $queryBuilder->createNamedParameter((int)$this->BE_USER->workspace, \PDO::PARAM_INT)
5413
                    )
5414
                );
5415
            if ($isLocalizedPage) {
5416
                // Add sys_language_uid = x restriction if discarding a localized page
5417
                $queryBuilder->andWhere(
5418
                    $queryBuilder->expr()->eq(
5419
                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
5420
                        $queryBuilder->createNamedParameter($sysLanguageId, \PDO::PARAM_INT)
5421
                    )
5422
                );
5423
            }
5424
            $statement = $queryBuilder->execute();
5425
            while ($row = $statement->fetch()) {
5426
                $this->discard($table, null, $row);
5427
            }
5428
        }
5429
    }
5430
5431
    /**
5432
     * Discard record relations like inline and MM of a record.
5433
     *
5434
     * @param string $table Table name of this record
5435
     * @param array $record The record row to handle
5436
     */
5437
    protected function discardRecordRelations(string $table, array $record): void
5438
    {
5439
        foreach ($record as $field => $value) {
5440
            $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? null;
5441
            if (!isset($fieldConfig['type'])) {
5442
                continue;
5443
            }
5444
            if ($fieldConfig['type'] === 'inline') {
5445
                $foreignTable = $fieldConfig['foreign_table'] ?? null;
5446
                if (!$foreignTable
5447
                     || (isset($fieldConfig['behaviour']['enableCascadingDelete'])
5448
                        && (bool)$fieldConfig['behaviour']['enableCascadingDelete'] === false)
5449
                ) {
5450
                    continue;
5451
                }
5452
                $inlineType = $this->getInlineFieldType($fieldConfig);
5453
                if ($inlineType === 'list' || $inlineType === 'field') {
5454
                    $dbAnalysis = $this->createRelationHandlerInstance();
5455
                    $dbAnalysis->start($value, $fieldConfig['foreign_table'], '', (int)$record['uid'], $table, $fieldConfig);
5456
                    $dbAnalysis->undeleteRecord = true;
5457
                    foreach ($dbAnalysis->itemArray as $relationRecord) {
5458
                        $this->discard($relationRecord['table'], (int)$relationRecord['id']);
5459
                    }
5460
                }
5461
            } elseif ($this->isReferenceField($fieldConfig)) {
5462
                $allowedTables = $fieldConfig['type'] === 'group' ? $fieldConfig['allowed'] : $fieldConfig['foreign_table'];
5463
                $dbAnalysis = $this->createRelationHandlerInstance();
5464
                $dbAnalysis->start($value, $allowedTables, $fieldConfig['MM'], (int)$record['uid'], $table, $fieldConfig);
5465
                foreach ($dbAnalysis->itemArray as $relationRecord) {
5466
                    // @todo: Something should happen with these relations here ...
5467
                    // @todo: Can't use dropReferenceIndexRowsForRecord() here, this would drop sys_refindex entries we want to keep
5468
                    $this->updateRefIndex($relationRecord['table'], (int)$relationRecord['id']);
5469
                }
5470
            }
5471
        }
5472
    }
5473
5474
    /**
5475
     * Find localization overlays of a record and discard them.
5476
     *
5477
     * @param string $table Table of this record
5478
     * @param array $record Record row
5479
     */
5480
    protected function discardLocalizationOverlayRecords(string $table, array $record): void
5481
    {
5482
        if (!BackendUtility::isTableLocalizable($table)) {
5483
            return;
5484
        }
5485
        $uid = (int)$record['uid'];
5486
        $versionState = VersionState::cast($record['t3ver_state']);
5487
        if ($versionState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
5488
            // The t3ver_state=-1 record is incoming here. Localization overlays of this record have their uid field set
5489
            // to the uid of the t3ver_state=1 record, which is in the t3ver_oid field of the incoming record.
5490
            $uid = (int)$record['t3ver_oid'];
5491
        }
5492
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5493
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5494
        $statement = $queryBuilder->select('*')
5495
            ->from($table)
5496
            ->where(
5497
                $queryBuilder->expr()->eq(
5498
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5499
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5500
                ),
5501
                $queryBuilder->expr()->eq(
5502
                    't3ver_wsid',
5503
                    $queryBuilder->createNamedParameter((int)$this->BE_USER->workspace, \PDO::PARAM_INT)
5504
                )
5505
            )
5506
            ->execute();
5507
        while ($record = $statement->fetch()) {
5508
            $this->discard($table, null, $record);
5509
        }
5510
    }
5511
5512
    /*********************************************
5513
     *
5514
     * Cmd: Versioning
5515
     *
5516
     ********************************************/
5517
    /**
5518
     * Creates a new version of a record
5519
     * (Requires support in the table)
5520
     *
5521
     * @param string $table Table name
5522
     * @param int $id Record uid to versionize
5523
     * @param string $label Version label
5524
     * @param bool $delete If TRUE, the version is created to delete the record.
5525
     * @return int|null Returns the id of the new version (if any)
5526
     * @see copyRecord()
5527
     * @internal should only be used from within DataHandler
5528
     */
5529
    public function versionizeRecord($table, $id, $label, $delete = false)
5530
    {
5531
        $id = (int)$id;
5532
        // Stop any actions if the record is marked to be deleted:
5533
        // (this can occur if IRRE elements are versionized and child elements are removed)
5534
        if ($this->isElementToBeDeleted($table, $id)) {
5535
            return null;
5536
        }
5537
        if (!BackendUtility::isTableWorkspaceEnabled($table) || $id <= 0) {
5538
            $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, SystemLogErrorClassification::USER_ERROR);
5539
            return null;
5540
        }
5541
5542
        // Fetch record with permission check
5543
        $row = $this->recordInfoWithPermissionCheck($table, $id, Permission::PAGE_SHOW);
5544
5545
        // This checks if the record can be selected which is all that a copy action requires.
5546
        if ($row === false) {
5547
            $this->newlog(
5548
                'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "' . $table . ':' . $id . '"',
5549
                SystemLogErrorClassification::USER_ERROR
5550
            );
5551
            return null;
5552
        }
5553
5554
        // Record must be online record, otherwise we would create a version of a version
5555
        if (($row['t3ver_oid'] ?? 0) > 0) {
5556
            $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (record has an online ID)!', SystemLogErrorClassification::USER_ERROR);
5557
            return null;
5558
        }
5559
5560
        if ($delete && $this->cannotDeleteRecord($table, $id)) {
5561
            $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), SystemLogErrorClassification::USER_ERROR);
5562
            return null;
5563
        }
5564
5565
        // Set up the values to override when making a raw-copy:
5566
        $overrideArray = [
5567
            't3ver_oid' => $id,
5568
            't3ver_wsid' => $this->BE_USER->workspace,
5569
            't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5570
            't3ver_stage' => 0,
5571
        ];
5572
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5573
            $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5574
        }
5575
        // Checking if the record already has a version in the current workspace of the backend user
5576
        $versionRecord = ['uid' => null];
5577
        if ($this->BE_USER->workspace !== 0) {
5578
            // Look for version already in workspace:
5579
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5580
        }
5581
        // Create new version of the record and return the new uid
5582
        if (empty($versionRecord['uid'])) {
5583
            // Create raw-copy and return result:
5584
            // The information of the label to be used for the workspace record
5585
            // as well as the information whether the record shall be removed
5586
            // must be forwarded (creating delete placeholders on a workspace are
5587
            // done by copying the record and override several fields).
5588
            $workspaceOptions = [
5589
                'delete' => $delete,
5590
                'label' => $label,
5591
            ];
5592
            return $this->copyRecord_raw($table, $id, (int)$row['pid'], $overrideArray, $workspaceOptions);
5593
        }
5594
        // Reuse the existing record and return its uid
5595
        // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5596
        // did not make much sense since the information is available)
5597
        return $versionRecord['uid'];
5598
    }
5599
5600
    /**
5601
     * Swaps MM-relations for current/swap record, see version_swap()
5602
     *
5603
     * @param string $table Table for the two input records
5604
     * @param int $id Current record (about to go offline)
5605
     * @param int $swapWith Swap record (about to go online)
5606
     * @see version_swap()
5607
     * @internal should only be used from within DataHandler
5608
     */
5609
    public function version_remapMMForVersionSwap($table, $id, $swapWith)
5610
    {
5611
        // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5612
        $currentRec = BackendUtility::getRecord($table, $id);
5613
        $swapRec = BackendUtility::getRecord($table, $swapWith);
5614
        $this->version_remapMMForVersionSwap_reg = [];
5615
        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5616
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5617
            $conf = $fConf['config'];
5618
            if ($this->isReferenceField($conf)) {
5619
                $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5620
                $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5621
                if ($conf['MM']) {
5622
                    /** @var RelationHandler $dbAnalysis */
5623
                    $dbAnalysis = $this->createRelationHandlerInstance();
5624
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5625
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
0 ignored issues
show
Bug introduced by
It seems like $prependName can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\...andler::getValueArray() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5625
                    if (!empty($dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName))) {
Loading history...
5626
                        $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5627
                    }
5628
                    /** @var RelationHandler $dbAnalysis */
5629
                    $dbAnalysis = $this->createRelationHandlerInstance();
5630
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5631
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
5632
                        $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5633
                    }
5634
                }
5635
            } elseif ($conf['type'] === 'flex') {
5636
                // Current record
5637
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5638
                    $fConf,
5639
                    $table,
5640
                    $field,
5641
                    $currentRec
5642
                );
5643
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5644
                $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5645
                if (is_array($currentValueArray)) {
5646
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5647
                }
5648
                // Swap record
5649
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5650
                    $fConf,
5651
                    $table,
5652
                    $field,
5653
                    $swapRec
5654
                );
5655
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5656
                $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5657
                if (is_array($currentValueArray)) {
5658
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5659
                }
5660
            }
5661
        }
5662
        // Execute:
5663
        $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5664
    }
5665
5666
    /**
5667
     * Callback function for traversing the FlexForm structure in relation to ...
5668
     *
5669
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
5670
     * @param array $dsConf TCA field configuration (from Data Structure XML)
5671
     * @param string $dataValue The value of the flexForm field
5672
     * @param string $dataValue_ext1 Not used.
5673
     * @param string $dataValue_ext2 Not used.
5674
     * @param string $path Path in flexforms
5675
     * @see version_remapMMForVersionSwap()
5676
     * @see checkValue_flex_procInData_travDS()
5677
     * @internal should only be used from within DataHandler
5678
     */
5679
    public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
5680
    {
5681
        // Extract parameters:
5682
        [$table, $uid, $field] = $pParams;
5683
        if ($this->isReferenceField($dsConf)) {
5684
            $allowedTables = $dsConf['type'] === 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5685
            $prependName = $dsConf['type'] === 'group' ? $dsConf['prepend_tname'] : '';
5686
            if ($dsConf['MM']) {
5687
                /** @var RelationHandler $dbAnalysis */
5688
                $dbAnalysis = $this->createRelationHandlerInstance();
5689
                $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5690
                $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5691
            }
5692
        }
5693
    }
5694
5695
    /**
5696
     * Performing the remapping operations found necessary in version_remapMMForVersionSwap()
5697
     * It must be done in three steps with an intermediate "fake" uid. The UID can be something else than -$id (fx. 9999999+$id if you dare... :-)- as long as it is unique.
5698
     *
5699
     * @param string $table Table for the two input records
5700
     * @param int $id Current record (about to go offline)
5701
     * @param int $swapWith Swap record (about to go online)
5702
     * @see version_remapMMForVersionSwap()
5703
     * @internal should only be used from within DataHandler
5704
     */
5705
    public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
5706
    {
5707
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5708
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5709
                $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5710
            }
5711
        }
5712
        if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5713
            foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5714
                $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5715
            }
5716
        }
5717
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5718
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5719
                $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5720
            }
5721
        }
5722
    }
5723
5724
    /*********************************************
5725
     *
5726
     * Cmd: Helper functions
5727
     *
5728
     ********************************************/
5729
5730
    /**
5731
     * Returns an instance of DataHandler for handling local datamaps/cmdmaps
5732
     *
5733
     * @return DataHandler
5734
     */
5735
    protected function getLocalTCE()
5736
    {
5737
        $copyTCE = GeneralUtility::makeInstance(DataHandler::class, $this->referenceIndexUpdater);
5738
        $copyTCE->copyTree = $this->copyTree;
5739
        $copyTCE->enableLogging = $this->enableLogging;
5740
        // Transformations should NOT be carried out during copy
5741
        $copyTCE->dontProcessTransformations = true;
5742
        // make sure the isImporting flag is transferred, so all hooks know if
5743
        // the current process is an import process
5744
        $copyTCE->isImporting = $this->isImporting;
5745
        $copyTCE->bypassAccessCheckForRecords = $this->bypassAccessCheckForRecords;
5746
        $copyTCE->bypassWorkspaceRestrictions = $this->bypassWorkspaceRestrictions;
5747
        return $copyTCE;
5748
    }
5749
5750
    /**
5751
     * Processes the fields with references as registered during the copy process. This includes all FlexForm fields which had references.
5752
     * @internal should only be used from within DataHandler
5753
     */
5754
    public function remapListedDBRecords()
5755
    {
5756
        if (!empty($this->registerDBList)) {
5757
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5758
            foreach ($this->registerDBList as $table => $records) {
5759
                foreach ($records as $uid => $fields) {
5760
                    $newData = [];
5761
                    $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5762
                    $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5763
                    foreach ($fields as $fieldName => $value) {
5764
                        $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5765
                        switch ($conf['type']) {
5766
                            case 'group':
5767
                            case 'select':
5768
                                $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5769
                                if (is_array($vArray)) {
5770
                                    $newData[$fieldName] = implode(',', $vArray);
5771
                                }
5772
                                break;
5773
                            case 'flex':
5774
                                if ($value === 'FlexForm_reference') {
5775
                                    // This will fetch the new row for the element
5776
                                    $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5777
                                    if (is_array($origRecordRow)) {
5778
                                        BackendUtility::workspaceOL($table, $origRecordRow);
5779
                                        // Get current data structure and value array:
5780
                                        $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5781
                                            ['config' => $conf],
5782
                                            $table,
5783
                                            $fieldName,
5784
                                            $origRecordRow
5785
                                        );
5786
                                        $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5787
                                        $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5788
                                        // Do recursive processing of the XML data:
5789
                                        $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5790
                                        // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5791
                                        if (is_array($currentValueArray['data'])) {
5792
                                            $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5793
                                        }
5794
                                    }
5795
                                }
5796
                                break;
5797
                            case 'inline':
5798
                                $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5799
                                break;
5800
                            default:
5801
                                $this->logger->debug('Field type should not appear here: ' . $conf['type']);
5802
                        }
5803
                    }
5804
                    // If any fields were changed, those fields are updated!
5805
                    if (!empty($newData)) {
5806
                        $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5807
                    }
5808
                }
5809
            }
5810
        }
5811
    }
5812
5813
    /**
5814
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
5815
     *
5816
     * @param array $pParams Set of parameters in numeric array: table, uid, field
5817
     * @param array $dsConf TCA config for field (from Data Structure of course)
5818
     * @param string $dataValue Field value (from FlexForm XML)
5819
     * @param string $dataValue_ext1 Not used
5820
     * @param string $dataValue_ext2 Not used
5821
     * @return array Array where the "value" key carries the value.
5822
     * @see checkValue_flex_procInData_travDS()
5823
     * @see remapListedDBRecords()
5824
     * @internal should only be used from within DataHandler
5825
     */
5826
    public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
5827
    {
5828
        // Extract parameters:
5829
        [$table, $uid, $field] = $pParams;
5830
        // If references are set for this field, set flag so they can be corrected later:
5831
        if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5832
            $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5833
            if (is_array($vArray)) {
5834
                $dataValue = implode(',', $vArray);
5835
            }
5836
        }
5837
        // Return
5838
        return ['value' => $dataValue];
5839
    }
5840
5841
    /**
5842
     * Performs remapping of old UID values to NEW uid values for a DB reference field.
5843
     *
5844
     * @param array $conf TCA field config
5845
     * @param string $value Field value
5846
     * @param int $MM_localUid UID of local record (for MM relations - might need to change if support for FlexForms should be done!)
5847
     * @param string $table Table name
5848
     * @return array|null Returns array of items ready to implode for field content.
5849
     * @see remapListedDBRecords()
5850
     * @internal should only be used from within DataHandler
5851
     */
5852
    public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
5853
    {
5854
        // Initialize variables
5855
        // Will be set TRUE if an upgrade should be done...
5856
        $set = false;
5857
        // Allowed tables for references.
5858
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5859
        // Table name to prepend the UID
5860
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5861
        // Which tables that should possibly not be remapped
5862
        $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5863
        // Convert value to list of references:
5864
        $dbAnalysis = $this->createRelationHandlerInstance();
5865
        $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && $conf['allowNonIdValues'];
5866
        $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5867
        // Traverse those references and map IDs:
5868
        foreach ($dbAnalysis->itemArray as $k => $v) {
5869
            $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5870
            if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5871
                $dbAnalysis->itemArray[$k]['id'] = $mapID;
5872
                $set = true;
5873
            }
5874
        }
5875
        if (!empty($conf['MM'])) {
5876
            // Purge invalid items (live/version)
5877
            $dbAnalysis->purgeItemArray();
5878
            if ($dbAnalysis->isPurged()) {
5879
                $set = true;
5880
            }
5881
5882
            // If record has been versioned/copied in this process, handle invalid relations of the live record
5883
            $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5884
            $originalId = 0;
5885
            if (!empty($this->copyMappingArray_merged[$table])) {
5886
                $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5887
            }
5888
            if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5889
                $liveRelations = $this->createRelationHandlerInstance();
5890
                $liveRelations->setWorkspaceId(0);
5891
                $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5892
                // Purge invalid relations in the live workspace ("0")
5893
                $liveRelations->purgeItemArray(0);
5894
                if ($liveRelations->isPurged()) {
5895
                    $liveRelations->writeMM($conf['MM'], $liveId, $prependName);
0 ignored issues
show
Bug introduced by
It seems like $prependName can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\RelationHandler::writeMM() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5895
                    $liveRelations->writeMM($conf['MM'], $liveId, /** @scrutinizer ignore-type */ $prependName);
Loading history...
5896
                }
5897
            }
5898
        }
5899
        // If a change has been done, set the new value(s)
5900
        if ($set) {
5901
            if ($conf['MM']) {
5902
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5903
            } else {
5904
                return $dbAnalysis->getValueArray($prependName);
0 ignored issues
show
Bug introduced by
It seems like $prependName can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\...andler::getValueArray() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5904
                return $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName);
Loading history...
5905
            }
5906
        }
5907
        return null;
5908
    }
5909
5910
    /**
5911
     * Performs remapping of old UID values to NEW uid values for an inline field.
5912
     *
5913
     * @param array $conf TCA field config
5914
     * @param string $value Field value
5915
     * @param int $uid The uid of the ORIGINAL record
5916
     * @param string $table Table name
5917
     * @internal should only be used from within DataHandler
5918
     */
5919
    public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
5920
    {
5921
        $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5922
        if ($conf['foreign_table']) {
5923
            $inlineType = $this->getInlineFieldType($conf);
5924
            if ($inlineType === 'mm') {
5925
                $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5926
            } elseif ($inlineType !== false) {
5927
                /** @var RelationHandler $dbAnalysis */
5928
                $dbAnalysis = $this->createRelationHandlerInstance();
5929
                $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5930
5931
                $updatePidForRecords = [];
5932
                // Update values for specific versioned records
5933
                foreach ($dbAnalysis->itemArray as &$item) {
5934
                    $updatePidForRecords[$item['table']][] = $item['id'];
5935
                    $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5936
                    if ($versionedId !== null) {
5937
                        $updatePidForRecords[$item['table']][] = $versionedId;
5938
                        $item['id'] = $versionedId;
5939
                    }
5940
                }
5941
5942
                // Update child records if using pointer fields ('foreign_field'):
5943
                if ($inlineType === 'field') {
5944
                    $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
5945
                }
5946
                $thePidToUpdate = null;
5947
                // If the current field is set on a page record, update the pid of related child records:
5948
                if ($table === 'pages') {
5949
                    $thePidToUpdate = $theUidToUpdate;
5950
                } elseif (isset($this->registerDBPids[$table][$uid])) {
5951
                    $thePidToUpdate = $this->registerDBPids[$table][$uid];
5952
                    $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
5953
                }
5954
5955
                // Update child records if change to pid is required
5956
                if ($thePidToUpdate && !empty($updatePidForRecords)) {
5957
                    // Ensure that only the default language page is used as PID
5958
                    $thePidToUpdate = $this->getDefaultLanguagePageId($thePidToUpdate);
5959
                    // @todo: this can probably go away
5960
                    // ensure, only live page ids are used as 'pid' values
5961
                    $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
5962
                    if ($liveId !== null) {
5963
                        $thePidToUpdate = $liveId;
5964
                    }
5965
                    $updateValues = ['pid' => $thePidToUpdate];
5966
                    foreach ($updatePidForRecords as $tableName => $uids) {
5967
                        if (empty($tableName) || empty($uids)) {
5968
                            continue;
5969
                        }
5970
                        $conn = GeneralUtility::makeInstance(ConnectionPool::class)
5971
                            ->getConnectionForTable($tableName);
5972
                        foreach ($uids as $updateUid) {
5973
                            $conn->update($tableName, $updateValues, ['uid' => $updateUid]);
5974
                        }
5975
                    }
5976
                }
5977
            }
5978
        }
5979
    }
5980
5981
    /**
5982
     * Processes the $this->remapStack at the end of copying, inserting, etc. actions.
5983
     * The remapStack takes care about the correct mapping of new and old uids in case of relational data.
5984
     * @internal should only be used from within DataHandler
5985
     */
5986
    public function processRemapStack()
5987
    {
5988
        // Processes the remap stack:
5989
        if (is_array($this->remapStack)) {
0 ignored issues
show
introduced by
The condition is_array($this->remapStack) is always true.
Loading history...
5990
            $remapFlexForms = [];
5991
            $hookPayload = [];
5992
5993
            $newValue = null;
5994
            foreach ($this->remapStack as $remapAction) {
5995
                // If no position index for the arguments was set, skip this remap action:
5996
                if (!is_array($remapAction['pos'])) {
5997
                    continue;
5998
                }
5999
                // Load values from the argument array in remapAction:
6000
                $field = $remapAction['field'];
6001
                $id = $remapAction['args'][$remapAction['pos']['id']];
6002
                $rawId = $id;
6003
                $table = $remapAction['args'][$remapAction['pos']['table']];
6004
                $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
6005
                $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
6006
                $additionalData = $remapAction['additionalData'];
6007
                // The record is new and has one or more new ids (in case of versioning/workspaces):
6008
                if (strpos($id, 'NEW') !== false) {
6009
                    // Replace NEW...-ID with real uid:
6010
                    $id = $this->substNEWwithIDs[$id];
6011
                    // If the new parent record is on a non-live workspace or versionized, it has another new id:
6012
                    if (isset($this->autoVersionIdMap[$table][$id])) {
6013
                        $id = $this->autoVersionIdMap[$table][$id];
6014
                    }
6015
                    $remapAction['args'][$remapAction['pos']['id']] = $id;
6016
                }
6017
                // Replace relations to NEW...-IDs in field value (uids of child records):
6018
                if (is_array($valueArray)) {
6019
                    foreach ($valueArray as $key => $value) {
6020
                        if (strpos($value, 'NEW') !== false) {
6021
                            if (strpos($value, '_') === false) {
6022
                                $affectedTable = $tcaFieldConf['foreign_table'];
6023
                                $prependTable = false;
6024
                            } else {
6025
                                $parts = explode('_', $value);
6026
                                $value = array_pop($parts);
6027
                                $affectedTable = implode('_', $parts);
6028
                                $prependTable = true;
6029
                            }
6030
                            $value = $this->substNEWwithIDs[$value];
6031
                            // The record is new, but was also auto-versionized and has another new id:
6032
                            if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
6033
                                $value = $this->autoVersionIdMap[$affectedTable][$value];
6034
                            }
6035
                            if ($prependTable) {
6036
                                $value = $affectedTable . '_' . $value;
6037
                            }
6038
                            // Set a hint that this was a new child record:
6039
                            $this->newRelatedIDs[$affectedTable][] = $value;
6040
                            $valueArray[$key] = $value;
6041
                        }
6042
                    }
6043
                    $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
6044
                }
6045
                // Process the arguments with the defined function:
6046
                if (!empty($remapAction['func'])) {
6047
                    $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
6048
                }
6049
                // If array is returned, check for maxitems condition, if string is returned this was already done:
6050
                if (is_array($newValue)) {
6051
                    $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
6052
                    // The reference casting is only required if
6053
                    // checkValue_group_select_processDBdata() returns an array
6054
                    $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
6055
                }
6056
                // Update in database (list of children (csv) or number of relations (foreign_field)):
6057
                if (!empty($field)) {
6058
                    $fieldArray = [$field => $newValue];
6059
                    if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
6060
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
6061
                    }
6062
                    $this->updateDB($table, $id, $fieldArray);
6063
                } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
6064
                    // Collect data to update FlexForms
6065
                    $flexFormId = $additionalData['flexFormId'];
6066
                    $flexFormPath = $additionalData['flexFormPath'];
6067
6068
                    if (!isset($remapFlexForms[$flexFormId])) {
6069
                        $remapFlexForms[$flexFormId] = [];
6070
                    }
6071
6072
                    $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
6073
                }
6074
6075
                // Collect elements that shall trigger processDatamap_afterDatabaseOperations
6076
                if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
6077
                    $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
6078
                    if (!isset($hookPayload[$table][$rawId])) {
6079
                        $hookPayload[$table][$rawId] = [
6080
                            'status' => $hookArgs['status'],
6081
                            'fieldArray' => $hookArgs['fieldArray'],
6082
                            'hookObjects' => $hookArgs['hookObjectsArr'],
6083
                        ];
6084
                    }
6085
                    $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
6086
                }
6087
            }
6088
6089
            if ($remapFlexForms) {
6090
                foreach ($remapFlexForms as $flexFormId => $modifications) {
6091
                    $this->updateFlexFormData($flexFormId, $modifications);
6092
                }
6093
            }
6094
6095
            foreach ($hookPayload as $tableName => $rawIdPayload) {
6096
                foreach ($rawIdPayload as $rawId => $payload) {
6097
                    foreach ($payload['hookObjects'] as $hookObject) {
6098
                        if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
6099
                            continue;
6100
                        }
6101
                        $hookObject->processDatamap_afterDatabaseOperations(
6102
                            $payload['status'],
6103
                            $tableName,
6104
                            $rawId,
6105
                            $payload['fieldArray'],
6106
                            $this
6107
                        );
6108
                    }
6109
                }
6110
            }
6111
        }
6112
        // Processes the remap stack actions:
6113
        if ($this->remapStackActions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->remapStackActions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
6114
            foreach ($this->remapStackActions as $action) {
6115
                if (isset($action['callback'], $action['arguments'])) {
6116
                    call_user_func_array($action['callback'], $action['arguments']);
6117
                }
6118
            }
6119
        }
6120
        // Reset:
6121
        $this->remapStack = [];
6122
        $this->remapStackRecords = [];
6123
        $this->remapStackActions = [];
6124
    }
6125
6126
    /**
6127
     * Updates FlexForm data.
6128
     *
6129
     * @param string $flexFormId e.g. <table>:<uid>:<field>
6130
     * @param array $modifications Modifications with paths and values (e.g. 'sDEF/lDEV/field/vDEF' => 'TYPO3')
6131
     */
6132
    protected function updateFlexFormData($flexFormId, array $modifications)
6133
    {
6134
        [$table, $uid, $field] = explode(':', $flexFormId, 3);
6135
6136
        if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
6137
            $uid = $this->substNEWwithIDs[$uid];
6138
        }
6139
6140
        $record = $this->recordInfo($table, $uid, '*');
6141
6142
        if (!$table || !$uid || !$field || !is_array($record)) {
6143
            return;
6144
        }
6145
6146
        BackendUtility::workspaceOL($table, $record);
6147
6148
        // Get current data structure and value array:
6149
        $valueStructure = GeneralUtility::xml2array($record[$field]);
6150
6151
        // Do recursive processing of the XML data:
6152
        foreach ($modifications as $path => $value) {
6153
            $valueStructure['data'] = ArrayUtility::setValueByPath(
6154
                $valueStructure['data'],
6155
                $path,
6156
                $value
6157
            );
6158
        }
6159
6160
        if (is_array($valueStructure['data'])) {
6161
            // The return value should be compiled back into XML
6162
            $values = [
6163
                $field => $this->checkValue_flexArray2Xml($valueStructure, true),
6164
            ];
6165
6166
            $this->updateDB($table, $uid, $values);
6167
        }
6168
    }
6169
6170
    /**
6171
     * Triggers a remap action for a specific record.
6172
     *
6173
     * Some records are post-processed by the processRemapStack() method (e.g. IRRE children).
6174
     * This method determines whether an action/modification is executed directly to a record
6175
     * or is postponed to happen after remapping data.
6176
     *
6177
     * @param string $table Name of the table
6178
     * @param string $id Id of the record (can also be a "NEW..." string)
6179
     * @param array $callback The method to be called
6180
     * @param array $arguments The arguments to be submitted to the callback method
6181
     * @param bool $forceRemapStackActions Whether to force to use the stack
6182
     * @see processRemapStack
6183
     */
6184
    protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
6185
    {
6186
        // Check whether the affected record is marked to be remapped:
6187
        if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
6188
            call_user_func_array($callback, $arguments);
6189
        } else {
6190
            $this->addRemapAction($table, $id, $callback, $arguments);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Core\DataHandl...ndler::addRemapAction(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

6190
            $this->addRemapAction($table, /** @scrutinizer ignore-type */ $id, $callback, $arguments);
Loading history...
6191
        }
6192
    }
6193
6194
    /**
6195
     * Adds an instruction to the remap action stack (used with IRRE).
6196
     *
6197
     * @param string $table The affected table
6198
     * @param int $id The affected ID
6199
     * @param array $callback The callback information (object and method)
6200
     * @param array $arguments The arguments to be used with the callback
6201
     * @internal should only be used from within DataHandler
6202
     */
6203
    public function addRemapAction($table, $id, array $callback, array $arguments)
6204
    {
6205
        $this->remapStackActions[] = [
6206
            'affects' => [
6207
                'table' => $table,
6208
                'id' => $id
6209
            ],
6210
            'callback' => $callback,
6211
            'arguments' => $arguments
6212
        ];
6213
    }
6214
6215
    /**
6216
     * If a parent record was versionized on a workspace in $this->process_datamap,
6217
     * it might be possible, that child records (e.g. on using IRRE) were affected.
6218
     * This function finds these relations and updates their uids in the $incomingFieldArray.
6219
     * The $incomingFieldArray is updated by reference!
6220
     *
6221
     * @param string $table Table name of the parent record
6222
     * @param int $id Uid of the parent record
6223
     * @param array $incomingFieldArray Reference to the incomingFieldArray of process_datamap
6224
     * @param array $registerDBList Reference to the $registerDBList array that was created/updated by versionizing calls to DataHandler in process_datamap.
6225
     * @internal should only be used from within DataHandler
6226
     */
6227
    public function getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
6228
    {
6229
        if (is_array($registerDBList[$table][$id])) {
6230
            foreach ($incomingFieldArray as $field => $value) {
6231
                $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
6232
                if ($registerDBList[$table][$id][$field] && ($foreignTable = $fieldConf['foreign_table'])) {
6233
                    $newValueArray = [];
6234
                    $origValueArray = is_array($value) ? $value : explode(',', $value);
6235
                    // Update the uids of the copied records, but also take care about new records:
6236
                    foreach ($origValueArray as $childId) {
6237
                        $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ?: $childId;
6238
                    }
6239
                    // Set the changed value to the $incomingFieldArray
6240
                    $incomingFieldArray[$field] = implode(',', $newValueArray);
6241
                }
6242
            }
6243
            // Clean up the $registerDBList array:
6244
            unset($registerDBList[$table][$id]);
6245
            if (empty($registerDBList[$table])) {
6246
                unset($registerDBList[$table]);
6247
            }
6248
        }
6249
    }
6250
6251
    /**
6252
     * Simple helper method to hard delete one row from table ignoring delete TCA field
6253
     *
6254
     * @param string $table A row from this table should be deleted
6255
     * @param int $uid Uid of row to be deleted
6256
     */
6257
    protected function hardDeleteSingleRecord(string $table, int $uid): void
6258
    {
6259
        GeneralUtility::makeInstance(ConnectionPool::class)
6260
            ->getConnectionForTable($table)
6261
            ->delete($table, ['uid' => $uid], [\PDO::PARAM_INT]);
6262
    }
6263
6264
    /*****************************
6265
     *
6266
     * Access control / Checking functions
6267
     *
6268
     *****************************/
6269
    /**
6270
     * Checking group modify_table access list
6271
     *
6272
     * @param string $table Table name
6273
     * @return bool Returns TRUE if the user has general access to modify the $table
6274
     * @internal should only be used from within DataHandler
6275
     */
6276
    public function checkModifyAccessList($table)
6277
    {
6278
        $res = $this->admin || (!$this->tableAdminOnly($table) && isset($this->BE_USER->groupData['tables_modify']) && GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table));
6279
        // Hook 'checkModifyAccessList': Post-processing of the state of access
6280
        foreach ($this->getCheckModifyAccessListHookObjects() as $hookObject) {
6281
            /** @var DataHandlerCheckModifyAccessListHookInterface $hookObject */
6282
            $hookObject->checkModifyAccessList($res, $table, $this);
6283
        }
6284
        return $res;
6285
    }
6286
6287
    /**
6288
     * Checking if a record with uid $id from $table is in the BE_USERS webmounts which is required for editing etc.
6289
     *
6290
     * @param string $table Table name
6291
     * @param int $id UID of record
6292
     * @return bool Returns TRUE if OK. Cached results.
6293
     * @internal should only be used from within DataHandler
6294
     */
6295
    public function isRecordInWebMount($table, $id)
6296
    {
6297
        if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
6298
            $recP = $this->getRecordProperties($table, $id);
6299
            $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
6300
        }
6301
        return $this->isRecordInWebMount_Cache[$table . ':' . $id];
6302
    }
6303
6304
    /**
6305
     * Checks if the input page ID is in the BE_USER webmounts
6306
     *
6307
     * @param int $pid Page ID to check
6308
     * @return bool TRUE if OK. Cached results.
6309
     * @internal should only be used from within DataHandler
6310
     */
6311
    public function isInWebMount($pid)
6312
    {
6313
        if (!isset($this->isInWebMount_Cache[$pid])) {
6314
            $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
6315
        }
6316
        return $this->isInWebMount_Cache[$pid];
6317
    }
6318
6319
    /**
6320
     * Checks if user may update a record with uid=$id from $table
6321
     *
6322
     * @param string $table Record table
6323
     * @param int $id Record UID
6324
     * @param array|bool $data Record data
6325
     * @param array $hookObjectsArr Hook objects
6326
     * @return bool Returns TRUE if the user may update the record given by $table and $id
6327
     * @internal should only be used from within DataHandler
6328
     */
6329
    public function checkRecordUpdateAccess($table, $id, $data = false, $hookObjectsArr = null)
6330
    {
6331
        $res = null;
6332
        if (is_array($hookObjectsArr)) {
6333
            foreach ($hookObjectsArr as $hookObj) {
6334
                if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
6335
                    $res = $hookObj->checkRecordUpdateAccess($table, $id, $data, $res, $this);
6336
                }
6337
            }
6338
            if (isset($res)) {
6339
                return (bool)$res;
6340
            }
6341
        }
6342
        $res = false;
6343
6344
        if ($GLOBALS['TCA'][$table] && (int)$id > 0) {
6345
            $cacheId = 'checkRecordUpdateAccess_' . $table . '_' . $id;
6346
6347
            // If information is cached, return it
6348
            $cachedValue = $this->runtimeCache->get($cacheId);
6349
            if (!empty($cachedValue)) {
6350
                return $cachedValue;
6351
            }
6352
6353
            if ($table === 'pages' || ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap))) {
6354
                // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6355
                $perms = Permission::PAGE_EDIT;
6356
            } else {
6357
                $perms = Permission::CONTENT_EDIT;
6358
            }
6359
            if ($this->doesRecordExist($table, $id, $perms)) {
6360
                $res = 1;
6361
            }
6362
            // Cache the result
6363
            $this->runtimeCache->set($cacheId, $res);
6364
        }
6365
        return $res;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $res also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
6366
    }
6367
6368
    /**
6369
     * Checks if user may insert a record from $insertTable on $pid
6370
     *
6371
     * @param string $insertTable Tablename to check
6372
     * @param int $pid Integer PID
6373
     * @param int $action For logging: Action number.
6374
     * @return bool Returns TRUE if the user may insert a record from table $insertTable on page $pid
6375
     * @internal should only be used from within DataHandler
6376
     */
6377
    public function checkRecordInsertAccess($insertTable, $pid, $action = SystemLogDatabaseAction::INSERT)
6378
    {
6379
        $pid = (int)$pid;
6380
        if ($pid < 0) {
6381
            return false;
6382
        }
6383
        // If information is cached, return it
6384
        if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
6385
            return $this->recInsertAccessCache[$insertTable][$pid];
6386
        }
6387
6388
        $res = false;
6389
        if ($insertTable === 'pages') {
6390
            $perms = Permission::PAGE_NEW;
6391
        } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
6392
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6393
            $perms = Permission::PAGE_EDIT;
6394
        } else {
6395
            $perms = Permission::CONTENT_EDIT;
6396
        }
6397
        $pageExists = (bool)$this->doesRecordExist('pages', $pid, $perms);
6398
        // If either admin and root-level or if page record exists and 1) if 'pages' you may create new ones 2) if page-content, new content items may be inserted on the $pid page
6399
        if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
6400
            // Check permissions
6401
            if ($this->isTableAllowedForThisPage($pid, $insertTable)) {
6402
                $res = true;
6403
                // Cache the result
6404
                $this->recInsertAccessCache[$insertTable][$pid] = $res;
6405
            } elseif ($this->enableLogging) {
6406
                $propArr = $this->getRecordProperties('pages', $pid);
6407
                $this->log($insertTable, $pid, $action, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to insert record on page \'%s\' (%s) where this table, %s, is not allowed', 11, [$propArr['header'], $pid, $insertTable], $propArr['event_pid']);
6408
            }
6409
        } elseif ($this->enableLogging) {
6410
            $propArr = $this->getRecordProperties('pages', $pid);
6411
            $this->log($insertTable, $pid, $action, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to insert a record on page \'%s\' (%s) from table \'%s\' without permissions. Or non-existing page.', 12, [$propArr['header'], $pid, $insertTable], $propArr['event_pid']);
6412
        }
6413
        return $res;
6414
    }
6415
6416
    /**
6417
     * Checks if a table is allowed on a certain page id according to allowed tables set for the page "doktype" and its [ctrl][rootLevel]-settings if any.
6418
     *
6419
     * @param int $page_uid Page id for which to check, including 0 (zero) if checking for page tree root.
6420
     * @param string $checkTable Table name to check
6421
     * @return bool TRUE if OK
6422
     * @internal should only be used from within DataHandler
6423
     */
6424
    public function isTableAllowedForThisPage($page_uid, $checkTable)
6425
    {
6426
        $page_uid = (int)$page_uid;
6427
        $rootLevelSetting = (int)$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'];
6428
        // Check if rootLevel flag is set and we're trying to insert on rootLevel - and reversed - and that the table is not "pages" which are allowed anywhere.
6429
        if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$page_uid)) {
6430
            return false;
6431
        }
6432
        $allowed = false;
6433
        // Check root-level
6434
        if (!$page_uid) {
6435
            if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
6436
                $allowed = true;
6437
            }
6438
        } else {
6439
            // Check non-root-level
6440
            $doktype = $this->pageInfo($page_uid, 'doktype');
6441
            $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6442
            $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6443
            // If all tables or the table is listed as an allowed type, return TRUE
6444
            if (strpos($allowedTableList, '*') !== false || in_array($checkTable, $allowedArray, true)) {
6445
                $allowed = true;
6446
            }
6447
        }
6448
        return $allowed;
6449
    }
6450
6451
    /**
6452
     * Checks if record can be selected based on given permission criteria
6453
     *
6454
     * @param string $table Record table name
6455
     * @param int $id Record UID
6456
     * @param int $perms Permission restrictions to observe: integer that will be bitwise AND'ed.
6457
     * @return bool Returns TRUE if the record given by $table, $id and $perms can be selected
6458
     *
6459
     * @throws \RuntimeException
6460
     * @internal should only be used from within DataHandler
6461
     */
6462
    public function doesRecordExist($table, $id, int $perms)
6463
    {
6464
        return $this->recordInfoWithPermissionCheck($table, $id, $perms, 'uid, pid') !== false;
6465
    }
6466
6467
    /**
6468
     * Looks up a page based on permissions.
6469
     *
6470
     * @param int $id Page id
6471
     * @param int $perms Permission integer
6472
     * @param array $columns Columns to select
6473
     * @return bool|array
6474
     * @internal
6475
     * @see doesRecordExist()
6476
     */
6477
    protected function doesRecordExist_pageLookUp($id, $perms, $columns = ['uid'])
6478
    {
6479
        $permission = new Permission($perms);
6480
        $cacheId = md5('doesRecordExist_pageLookUp_' . $id . '_' . $perms . '_' . implode(
6481
            '_',
6482
            $columns
6483
        ) . '_' . (string)$this->admin);
6484
6485
        // If result is cached, return it
6486
        $cachedResult = $this->runtimeCache->get($cacheId);
6487
        if (!empty($cachedResult)) {
6488
            return $cachedResult;
6489
        }
6490
6491
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6492
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6493
        $queryBuilder
6494
            ->select(...$columns)
6495
            ->from('pages')
6496
            ->where($queryBuilder->expr()->eq(
6497
                'uid',
6498
                $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
6499
            ));
6500
        if (!$permission->nothingIsGranted() && !$this->admin) {
6501
            $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($perms));
6502
        }
6503
        if (!$this->admin && $GLOBALS['TCA']['pages']['ctrl']['editlock'] &&
6504
            ($permission->editPagePermissionIsGranted() || $permission->deletePagePermissionIsGranted() || $permission->editContentPermissionIsGranted())
6505
        ) {
6506
            $queryBuilder->andWhere($queryBuilder->expr()->eq(
6507
                $GLOBALS['TCA']['pages']['ctrl']['editlock'],
6508
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
6509
            ));
6510
        }
6511
6512
        $row = $queryBuilder->execute()->fetch();
6513
        $this->runtimeCache->set($cacheId, $row);
6514
6515
        return $row;
6516
    }
6517
6518
    /**
6519
     * Checks if a whole branch of pages exists
6520
     *
6521
     * Tests the branch under $pid like doesRecordExist(), but it doesn't test the page with $pid as uid - use doesRecordExist() for this purpose.
6522
     * If $recurse is set, the function will follow subpages. This MUST be set, if we need the id-list for deleting pages or else we get an incomplete list
6523
     *
6524
     * @param string $inList List of page uids, this is added to and returned in the end
6525
     * @param int $pid Page ID to select subpages from.
6526
     * @param int $perms Perms integer to check each page record for.
6527
     * @param bool $recurse Recursion flag: If set, it will go out through the branch.
6528
     * @return string|int List of page IDs in branch, if there are subpages, empty string if there are none or -1 if no permission
6529
     * @internal should only be used from within DataHandler
6530
     */
6531
    public function doesBranchExist($inList, $pid, $perms, $recurse)
6532
    {
6533
        $pid = (int)$pid;
6534
        $perms = (int)$perms;
6535
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6536
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6537
        $result = $queryBuilder
6538
            ->select('uid', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody')
6539
            ->from('pages')
6540
            ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
6541
            ->orderBy('sorting')
6542
            ->execute();
6543
        while ($row = $result->fetch()) {
6544
            // IF admin, then it's OK
6545
            if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $perms)) {
6546
                $inList .= $row['uid'] . ',';
6547
                if ($recurse) {
6548
                    // Follow the subpages recursively...
6549
                    $inList = $this->doesBranchExist($inList, $row['uid'], $perms, $recurse);
6550
                    if ($inList === -1) {
6551
                        return -1;
6552
                    }
6553
                }
6554
            } else {
6555
                // No permissions
6556
                return -1;
6557
            }
6558
        }
6559
        return $inList;
6560
    }
6561
6562
    /**
6563
     * Checks if the $table is readOnly
6564
     *
6565
     * @param string $table Table name
6566
     * @return bool TRUE, if readonly
6567
     * @internal should only be used from within DataHandler
6568
     */
6569
    public function tableReadOnly($table)
6570
    {
6571
        // Returns TRUE if table is readonly
6572
        return (bool)$GLOBALS['TCA'][$table]['ctrl']['readOnly'];
6573
    }
6574
6575
    /**
6576
     * Checks if the $table is only editable by admin-users
6577
     *
6578
     * @param string $table Table name
6579
     * @return bool TRUE, if readonly
6580
     * @internal should only be used from within DataHandler
6581
     */
6582
    public function tableAdminOnly($table)
6583
    {
6584
        // Returns TRUE if table is admin-only
6585
        return !empty($GLOBALS['TCA'][$table]['ctrl']['adminOnly']);
6586
    }
6587
6588
    /**
6589
     * Checks if page $id is a uid in the rootline of page id $destinationId
6590
     * Used when moving a page
6591
     *
6592
     * @param int $destinationId Destination Page ID to test
6593
     * @param int $id Page ID to test for presence inside Destination
6594
     * @return bool Returns FALSE if ID is inside destination (including equal to)
6595
     * @internal should only be used from within DataHandler
6596
     */
6597
    public function destNotInsideSelf($destinationId, $id)
6598
    {
6599
        $loopCheck = 100;
6600
        $destinationId = (int)$destinationId;
6601
        $id = (int)$id;
6602
        if ($destinationId === $id) {
6603
            return false;
6604
        }
6605
        while ($destinationId !== 0 && $loopCheck > 0) {
6606
            $loopCheck--;
6607
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6608
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6609
            $result = $queryBuilder
6610
                ->select('pid', 'uid', 't3ver_oid', 't3ver_wsid')
6611
                ->from('pages')
6612
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($destinationId, \PDO::PARAM_INT)))
6613
                ->execute();
6614
            if ($row = $result->fetch()) {
6615
                BackendUtility::fixVersioningPid('pages', $row);
6616
                if ($row['pid'] == $id) {
6617
                    return false;
6618
                }
6619
                $destinationId = (int)$row['pid'];
6620
            } else {
6621
                return false;
6622
            }
6623
        }
6624
        return true;
6625
    }
6626
6627
    /**
6628
     * Generate an array of fields to be excluded from editing for the user. Based on "exclude"-field in TCA and a look up in non_exclude_fields
6629
     * Will also generate this list for admin-users so they must be check for before calling the function
6630
     *
6631
     * @return array Array of [table]-[field] pairs to exclude from editing.
6632
     * @internal should only be used from within DataHandler
6633
     */
6634
    public function getExcludeListArray()
6635
    {
6636
        $list = [];
6637
        if (isset($this->BE_USER->groupData['non_exclude_fields'])) {
6638
            $nonExcludeFieldsArray = array_flip(GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
6639
            foreach ($GLOBALS['TCA'] as $table => $tableConfiguration) {
6640
                if (isset($tableConfiguration['columns'])) {
6641
                    foreach ($tableConfiguration['columns'] as $field => $config) {
6642
                        $isExcludeField = ($config['exclude'] ?? false);
6643
                        $isOnlyVisibleForAdmins = ($GLOBALS['TCA'][$table]['columns'][$field]['displayCond'] ?? '') === 'HIDE_FOR_NON_ADMINS';
6644
                        $editorHasPermissionForThisField = isset($nonExcludeFieldsArray[$table . ':' . $field]);
6645
                        if ($isOnlyVisibleForAdmins || ($isExcludeField && !$editorHasPermissionForThisField)) {
6646
                            $list[] = $table . '-' . $field;
6647
                        }
6648
                    }
6649
                }
6650
            }
6651
        }
6652
6653
        return $list;
6654
    }
6655
6656
    /**
6657
     * Checks if there are records on a page from tables that are not allowed
6658
     *
6659
     * @param int $page_uid Page ID
6660
     * @param int $doktype Page doktype
6661
     * @return bool|array Returns a list of the tables that are 'present' on the page but not allowed with the page_uid/doktype
6662
     * @internal should only be used from within DataHandler
6663
     */
6664
    public function doesPageHaveUnallowedTables($page_uid, $doktype)
6665
    {
6666
        $page_uid = (int)$page_uid;
6667
        if (!$page_uid) {
6668
            // Not a number. Probably a new page
6669
            return false;
6670
        }
6671
        $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6672
        // If all tables are allowed, return early
6673
        if (strpos($allowedTableList, '*') !== false) {
6674
            return false;
6675
        }
6676
        $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6677
        $tableList = [];
6678
        $allTableNames = $this->compileAdminTables();
6679
        foreach ($allTableNames as $table) {
6680
            // If the table is not in the allowed list, check if there are records...
6681
            if (in_array($table, $allowedArray, true)) {
6682
                continue;
6683
            }
6684
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6685
            $queryBuilder->getRestrictions()->removeAll();
6686
            $count = $queryBuilder
6687
                ->count('uid')
6688
                ->from($table)
6689
                ->where($queryBuilder->expr()->eq(
6690
                    'pid',
6691
                    $queryBuilder->createNamedParameter($page_uid, \PDO::PARAM_INT)
6692
                ))
6693
                ->execute()
6694
                ->fetchColumn(0);
6695
            if ($count) {
6696
                $tableList[] = $table;
6697
            }
6698
        }
6699
        return implode(',', $tableList);
0 ignored issues
show
Bug Best Practice introduced by
The expression return implode(',', $tableList) returns the type string which is incompatible with the documented return type array|boolean.
Loading history...
6700
    }
6701
6702
    /*****************************
6703
     *
6704
     * Information lookup
6705
     *
6706
     *****************************/
6707
    /**
6708
     * Returns the value of the $field from page $id
6709
     * NOTICE; the function caches the result for faster delivery next time. You can use this function repeatedly without performance loss since it doesn't look up the same record twice!
6710
     *
6711
     * @param int $id Page uid
6712
     * @param string $field Field name for which to return value
6713
     * @return string Value of the field. Result is cached in $this->pageCache[$id][$field] and returned from there next time!
6714
     * @internal should only be used from within DataHandler
6715
     */
6716
    public function pageInfo($id, $field)
6717
    {
6718
        if (!isset($this->pageCache[$id])) {
6719
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6720
            $queryBuilder->getRestrictions()->removeAll();
6721
            $row = $queryBuilder
6722
                ->select('*')
6723
                ->from('pages')
6724
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6725
                ->execute()
6726
                ->fetch();
6727
            if ($row) {
6728
                $this->pageCache[$id] = $row;
6729
            }
6730
        }
6731
        return $this->pageCache[$id][$field];
6732
    }
6733
6734
    /**
6735
     * Returns the row of a record given by $table and $id and $fieldList (list of fields, may be '*')
6736
     * NOTICE: No check for deleted or access!
6737
     *
6738
     * @param string $table Table name
6739
     * @param int $id UID of the record from $table
6740
     * @param string $fieldList Field list for the SELECT query, eg. "*" or "uid,pid,...
6741
     * @return array|null Returns the selected record on success, otherwise NULL.
6742
     * @internal should only be used from within DataHandler
6743
     */
6744
    public function recordInfo($table, $id, $fieldList)
6745
    {
6746
        // Skip, if searching for NEW records or there's no TCA table definition
6747
        if ((int)$id === 0 || !isset($GLOBALS['TCA'][$table])) {
6748
            return null;
6749
        }
6750
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6751
        $queryBuilder->getRestrictions()->removeAll();
6752
        $result = $queryBuilder
6753
            ->select(...GeneralUtility::trimExplode(',', $fieldList))
6754
            ->from($table)
6755
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6756
            ->execute()
6757
            ->fetch();
6758
        return $result ?: null;
6759
    }
6760
6761
    /**
6762
     * Checks if record exists with and without permission check and returns that row
6763
     *
6764
     * @param string $table Record table name
6765
     * @param int $id Record UID
6766
     * @param int $perms Permission restrictions to observe: An integer that will be bitwise AND'ed.
6767
     * @param string $fieldList - fields - default is '*'
6768
     * @throws \RuntimeException
6769
     * @return array|bool Row if exists and accessible, false otherwise
6770
     */
6771
    protected function recordInfoWithPermissionCheck(string $table, int $id, int $perms, string $fieldList = '*')
6772
    {
6773
        if ($this->bypassAccessCheckForRecords) {
6774
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6775
6776
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6777
            $queryBuilder->getRestrictions()->removeAll();
6778
6779
            $record = $queryBuilder->select(...$columns)
6780
                ->from($table)
6781
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6782
                ->execute()
6783
                ->fetch();
6784
6785
            return $record ?: false;
6786
        }
6787
        if (!$perms) {
6788
            throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
6789
        }
6790
        // For all tables: Check if record exists:
6791
        $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
6792
        if (is_array($GLOBALS['TCA'][$table]) && $id > 0 && ($this->admin || $isWebMountRestrictionIgnored || $this->isRecordInWebMount($table, $id))) {
6793
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6794
            if ($table !== 'pages') {
6795
                // Find record without checking page
6796
                // @todo: This should probably check for editlock
6797
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6798
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6799
                $output = $queryBuilder
6800
                    ->select(...$columns)
6801
                    ->from($table)
6802
                    ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6803
                    ->execute()
6804
                    ->fetch();
6805
                BackendUtility::fixVersioningPid($table, $output, true);
6806
                // If record found, check page as well:
6807
                if (is_array($output)) {
6808
                    // Looking up the page for record:
6809
                    $pageRec = $this->doesRecordExist_pageLookUp($output['pid'], $perms);
6810
                    // Return TRUE if either a page was found OR if the PID is zero AND the user is ADMIN (in which case the record is at root-level):
6811
                    $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
6812
                    if (is_array($pageRec) || !$output['pid'] && ($this->admin || $isRootLevelRestrictionIgnored)) {
6813
                        return $output;
6814
                    }
6815
                }
6816
                return false;
6817
            }
6818
            return $this->doesRecordExist_pageLookUp($id, $perms, $columns);
6819
        }
6820
        return false;
6821
    }
6822
6823
    /**
6824
     * Returns an array with record properties, like header and pid
6825
     * No check for deleted or access is done!
6826
     * For versionized records, pid is resolved to its live versions pid.
6827
     * Used for logging
6828
     *
6829
     * @param string $table Table name
6830
     * @param int $id Uid of record
6831
     * @param bool $noWSOL If set, no workspace overlay is performed
6832
     * @return array Properties of record
6833
     * @internal should only be used from within DataHandler
6834
     */
6835
    public function getRecordProperties($table, $id, $noWSOL = false)
6836
    {
6837
        $row = $table === 'pages' && !$id ? ['title' => '[root-level]', 'uid' => 0, 'pid' => 0] : $this->recordInfo($table, $id, '*');
6838
        if (!$noWSOL) {
6839
            BackendUtility::workspaceOL($table, $row);
6840
        }
6841
        return $this->getRecordPropertiesFromRow($table, $row);
6842
    }
6843
6844
    /**
6845
     * Returns an array with record properties, like header and pid, based on the row
6846
     *
6847
     * @param string $table Table name
6848
     * @param array $row Input row
6849
     * @return array|null Output array
6850
     * @internal should only be used from within DataHandler
6851
     */
6852
    public function getRecordPropertiesFromRow($table, $row)
6853
    {
6854
        if ($GLOBALS['TCA'][$table]) {
6855
            BackendUtility::fixVersioningPid($table, $row);
6856
            $liveUid = ($row['t3ver_oid'] ?? null) ? $row['t3ver_oid'] : $row['uid'];
6857
            return [
6858
                'header' => BackendUtility::getRecordTitle($table, $row),
6859
                'pid' => $row['pid'],
6860
                'event_pid' => $this->eventPid($table, (int)$liveUid, $row['pid']),
6861
                't3ver_state' => BackendUtility::isTableWorkspaceEnabled($table) ? $row['t3ver_state'] : '',
6862
                '_ORIG_pid' => $row['_ORIG_pid']
6863
            ];
6864
        }
6865
        return null;
6866
    }
6867
6868
    /**
6869
     * @param string $table
6870
     * @param int $uid
6871
     * @param int $pid
6872
     * @return int
6873
     * @internal should only be used from within DataHandler
6874
     */
6875
    public function eventPid($table, $uid, $pid)
6876
    {
6877
        return $table === 'pages' ? $uid : $pid;
6878
    }
6879
6880
    /*********************************************
6881
     *
6882
     * Storing data to Database Layer
6883
     *
6884
     ********************************************/
6885
    /**
6886
     * Update database record
6887
     * Does not check permissions but expects them to be verified on beforehand
6888
     *
6889
     * @param string $table Record table name
6890
     * @param int $id Record uid
6891
     * @param array $fieldArray Array of field=>value pairs to insert. FIELDS MUST MATCH the database FIELDS. No check is done.
6892
     * @internal should only be used from within DataHandler
6893
     */
6894
    public function updateDB($table, $id, $fieldArray)
6895
    {
6896
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && (int)$id) {
6897
            // Do NOT update the UID field, ever!
6898
            unset($fieldArray['uid']);
6899
            if (!empty($fieldArray)) {
6900
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6901
6902
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6903
6904
                $types = [];
6905
                $platform = $connection->getDatabasePlatform();
6906
                if ($platform instanceof SQLServerPlatform) {
6907
                    // mssql needs to set proper PARAM_LOB and others to update fields
6908
                    $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
6909
                    foreach ($fieldArray as $columnName => $columnValue) {
6910
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
6911
                    }
6912
                }
6913
6914
                // Execute the UPDATE query:
6915
                $updateErrorMessage = '';
6916
                try {
6917
                    $connection->update($table, $fieldArray, ['uid' => (int)$id], $types);
6918
                } catch (DBALException $e) {
6919
                    $updateErrorMessage = $e->getPrevious()->getMessage();
6920
                }
6921
                // If succeeds, do...:
6922
                if ($updateErrorMessage === '') {
6923
                    // Update reference index:
6924
                    $this->updateRefIndex($table, $id);
6925
                    // Set History data
6926
                    $historyEntryId = 0;
6927
                    if (isset($this->historyRecords[$table . ':' . $id])) {
6928
                        $historyEntryId = $this->getRecordHistoryStore()->modifyRecord($table, $id, $this->historyRecords[$table . ':' . $id], $this->correlationId);
6929
                    }
6930
                    if ($this->enableLogging) {
6931
                        if ($this->checkStoredRecords) {
6932
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, SystemLogDatabaseAction::UPDATE);
6933
                        } else {
6934
                            $newRow = $fieldArray;
6935
                            $newRow['uid'] = $id;
6936
                        }
6937
                        // Set log entry:
6938
                        $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6939
                        $isOfflineVersion = (bool)($newRow['t3ver_oid'] ?? 0);
6940
                        $this->log($table, $id, SystemLogDatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, 'Record \'%s\' (%s) was updated.' . ($isOfflineVersion ? ' (Offline version).' : ' (Online).'), 10, [$propArr['header'], $table . ':' . $id, 'history' => $historyEntryId], $propArr['event_pid']);
6941
                    }
6942
                    // Clear cache for relevant pages:
6943
                    $this->registerRecordIdForPageCacheClearing($table, $id);
6944
                    // Unset the pageCache for the id if table was page.
6945
                    if ($table === 'pages') {
6946
                        unset($this->pageCache[$id]);
6947
                    }
6948
                } else {
6949
                    $this->log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$updateErrorMessage, $table . ':' . $id]);
6950
                }
6951
            }
6952
        }
6953
    }
6954
6955
    /**
6956
     * Insert into database
6957
     * Does not check permissions but expects them to be verified on beforehand
6958
     *
6959
     * @param string $table Record table name
6960
     * @param string $id "NEW...." uid string
6961
     * @param array $fieldArray Array of field=>value pairs to insert. FIELDS MUST MATCH the database FIELDS. No check is done. "pid" must point to the destination of the record!
6962
     * @param bool $newVersion Set to TRUE if new version is created.
6963
     * @param int $suggestedUid Suggested UID value for the inserted record. See the array $this->suggestedInsertUids; Admin-only feature
6964
     * @param bool $dontSetNewIdIndex If TRUE, the ->substNEWwithIDs array is not updated. Only useful in very rare circumstances!
6965
     * @return int|null Returns ID on success.
6966
     * @internal should only be used from within DataHandler
6967
     */
6968
    public function insertDB($table, $id, $fieldArray, $newVersion = false, $suggestedUid = 0, $dontSetNewIdIndex = false)
6969
    {
6970
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && isset($fieldArray['pid'])) {
6971
            // Do NOT insert the UID field, ever!
6972
            unset($fieldArray['uid']);
6973
            if (!empty($fieldArray)) {
6974
                // Check for "suggestedUid".
6975
                // This feature is used by the import functionality to force a new record to have a certain UID value.
6976
                // This is only recommended for use when the destination server is a passive mirror of another server.
6977
                // As a security measure this feature is available only for Admin Users (for now)
6978
                $suggestedUid = (int)$suggestedUid;
6979
                if ($this->BE_USER->isAdmin() && $suggestedUid && $this->suggestedInsertUids[$table . ':' . $suggestedUid]) {
6980
                    // When the value of ->suggestedInsertUids[...] is "DELETE" it will try to remove the previous record
6981
                    if ($this->suggestedInsertUids[$table . ':' . $suggestedUid] === 'DELETE') {
6982
                        $this->hardDeleteSingleRecord($table, (int)$suggestedUid);
6983
                    }
6984
                    $fieldArray['uid'] = $suggestedUid;
6985
                }
6986
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6987
                $typeArray = [];
6988
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])
6989
                    && array_key_exists($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
6990
                ) {
6991
                    $typeArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = Connection::PARAM_LOB;
6992
                }
6993
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6994
                $insertErrorMessage = '';
6995
                try {
6996
                    // Execute the INSERT query:
6997
                    $connection->insert(
6998
                        $table,
6999
                        $fieldArray,
7000
                        $typeArray
7001
                    );
7002
                } catch (DBALException $e) {
7003
                    $insertErrorMessage = $e->getPrevious()->getMessage();
7004
                }
7005
                // If succees, do...:
7006
                if ($insertErrorMessage === '') {
7007
                    // Set mapping for NEW... -> real uid:
7008
                    // the NEW_id now holds the 'NEW....' -id
7009
                    $NEW_id = $id;
7010
                    $id = $this->postProcessDatabaseInsert($connection, $table, $suggestedUid);
7011
7012
                    if (!$dontSetNewIdIndex) {
7013
                        $this->substNEWwithIDs[$NEW_id] = $id;
7014
                        $this->substNEWwithIDs_table[$NEW_id] = $table;
7015
                    }
7016
                    $newRow = [];
7017
                    if ($this->enableLogging) {
7018
                        // Checking the record is properly saved if configured
7019
                        if ($this->checkStoredRecords) {
7020
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, SystemLogDatabaseAction::INSERT);
7021
                        } else {
7022
                            $newRow = $fieldArray;
7023
                            $newRow['uid'] = $id;
7024
                        }
7025
                    }
7026
                    // Update reference index:
7027
                    $this->updateRefIndex($table, $id);
7028
7029
                    // Store in history
7030
                    $this->getRecordHistoryStore()->addRecord($table, $id, $newRow, $this->correlationId);
0 ignored issues
show
Bug introduced by
It seems like $newRow can also be of type null; however, parameter $payload of TYPO3\CMS\Core\DataHandl...storyStore::addRecord() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7030
                    $this->getRecordHistoryStore()->addRecord($table, $id, /** @scrutinizer ignore-type */ $newRow, $this->correlationId);
Loading history...
7031
7032
                    if ($newVersion) {
7033
                        if ($this->enableLogging) {
7034
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7035
                            $this->log($table, $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::MESSAGE, 'New version created of table \'%s\', uid \'%s\'. UID of new version is \'%s\'', 10, [$table, $fieldArray['t3ver_oid'], $id], $propArr['event_pid'], $NEW_id);
7036
                        }
7037
                    } else {
7038
                        if ($this->enableLogging) {
7039
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7040
                            $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
7041
                            $this->log($table, $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::MESSAGE, 'Record \'%s\' (%s) was inserted on page \'%s\' (%s)', 10, [$propArr['header'], $table . ':' . $id, $page_propArr['header'], $newRow['pid']], $newRow['pid'], $NEW_id);
7042
                        }
7043
                        // Clear cache for relevant pages:
7044
                        $this->registerRecordIdForPageCacheClearing($table, $id);
7045
                    }
7046
                    return $id;
7047
                }
7048
                if ($this->enableLogging) {
7049
                    $this->log($table, $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $recuid of TYPO3\CMS\Core\DataHandling\DataHandler::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7049
                    $this->log($table, /** @scrutinizer ignore-type */ $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
Loading history...
7050
                }
7051
            }
7052
        }
7053
        return null;
7054
    }
7055
7056
    /**
7057
     * Checking stored record to see if the written values are properly updated.
7058
     *
7059
     * @param string $table Record table name
7060
     * @param int $id Record uid
7061
     * @param array $fieldArray Array of field=>value pairs to insert/update
7062
     * @param string $action Action, for logging only.
7063
     * @return array|null Selected row
7064
     * @see insertDB()
7065
     * @see updateDB()
7066
     * @internal should only be used from within DataHandler
7067
     */
7068
    public function checkStoredRecord($table, $id, $fieldArray, $action)
7069
    {
7070
        $id = (int)$id;
7071
        if (is_array($GLOBALS['TCA'][$table]) && $id) {
7072
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7073
            $queryBuilder->getRestrictions()->removeAll();
7074
7075
            $row = $queryBuilder
7076
                ->select('*')
7077
                ->from($table)
7078
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7079
                ->execute()
7080
                ->fetch();
7081
7082
            if (!empty($row)) {
7083
                // Traverse array of values that was inserted into the database and compare with the actually stored value:
7084
                $errors = [];
7085
                foreach ($fieldArray as $key => $value) {
7086
                    if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
7087
                        if (is_float($row[$key])) {
7088
                            // if the database returns the value as double, compare it as double
7089
                            if ((double)$value !== (double)$row[$key]) {
7090
                                $errors[] = $key;
7091
                            }
7092
                        } else {
7093
                            $dbType = $GLOBALS['TCA'][$table]['columns'][$key]['config']['dbType'] ?? false;
7094
                            if ($dbType === 'datetime' || $dbType === 'time') {
7095
                                $row[$key] = $this->normalizeTimeFormat($table, $row[$key], $dbType);
7096
                            }
7097
                            if ((string)$value !== (string)$row[$key]) {
7098
                                // The is_numeric check catches cases where we want to store a float/double value
7099
                                // and database returns the field as a string with the least required amount of
7100
                                // significant digits, i.e. "0.00" being saved and "0" being read back.
7101
                                if (is_numeric($value) && is_numeric($row[$key])) {
7102
                                    if ((double)$value === (double)$row[$key]) {
7103
                                        continue;
7104
                                    }
7105
                                }
7106
                                $errors[] = $key;
7107
                            }
7108
                        }
7109
                    }
7110
                }
7111
                // Set log message if there were fields with unmatching values:
7112
                if (!empty($errors)) {
7113
                    $message = sprintf(
7114
                        'These fields of record %d in table "%s" have not been saved correctly: %s! The values might have changed due to type casting of the database.',
7115
                        $id,
7116
                        $table,
7117
                        implode(', ', $errors)
7118
                    );
7119
                    $this->log($table, $id, $action, 0, SystemLogErrorClassification::USER_ERROR, $message);
0 ignored issues
show
Bug introduced by
$action of type string is incompatible with the type integer expected by parameter $action of TYPO3\CMS\Core\DataHandling\DataHandler::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7119
                    $this->log($table, $id, /** @scrutinizer ignore-type */ $action, 0, SystemLogErrorClassification::USER_ERROR, $message);
Loading history...
7120
                }
7121
                // Return selected rows:
7122
                return $row;
7123
            }
7124
        }
7125
        return null;
7126
    }
7127
7128
    /**
7129
     * Setting sys_history record, based on content previously set in $this->historyRecords[$table . ':' . $id] (by compareFieldArrayWithCurrentAndUnset())
7130
     *
7131
     * This functionality is now moved into the RecordHistoryStore and can be used instead.
7132
     *
7133
     * @param string $table Table name
7134
     * @param int $id Record ID
7135
     * @internal should only be used from within DataHandler
7136
     */
7137
    public function setHistory($table, $id)
7138
    {
7139
        if (isset($this->historyRecords[$table . ':' . $id])) {
7140
            $this->getRecordHistoryStore()->modifyRecord(
7141
                $table,
7142
                $id,
7143
                $this->historyRecords[$table . ':' . $id],
7144
                $this->correlationId
7145
            );
7146
        }
7147
    }
7148
7149
    /**
7150
     * @return RecordHistoryStore
7151
     */
7152
    protected function getRecordHistoryStore(): RecordHistoryStore
7153
    {
7154
        return GeneralUtility::makeInstance(
7155
            RecordHistoryStore::class,
7156
            RecordHistoryStore::USER_BACKEND,
7157
            $this->BE_USER->user['uid'],
7158
            $this->BE_USER->user['ses_backuserid'] ?? null,
7159
            $GLOBALS['EXEC_TIME'],
7160
            $this->BE_USER->workspace
7161
        );
7162
    }
7163
7164
    /**
7165
     * Register a table/uid combination in current user workspace for reference updating.
7166
     * Should be called on almost any update to a record which could affect references inside the record.
7167
     *
7168
     * @param string $table Table name
7169
     * @param int $uid Record UID
7170
     * @param int $workspace Workspace the record lives in
7171
     * @internal should only be used from within DataHandler
7172
     */
7173
    public function updateRefIndex($table, $uid, int $workspace = null): void
7174
    {
7175
        if ($workspace === null) {
7176
            $workspace = (int)$this->BE_USER->workspace;
7177
        }
7178
        $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)$uid, $workspace);
7179
    }
7180
7181
    /**
7182
     * Delete rows from sys_refindex a table / uid combination is involved in:
7183
     * Either on left side (tablename + recuid) OR right side (ref_table + ref_uid).
7184
     * Useful in scenarios like workspace-discard where parents or children are hard deleted: The
7185
     * expensive updateRefIndex() does not need to be called since we can just drop straight ahead.
7186
     *
7187
     * @param string $table Table name, used as tablename and ref_table
7188
     * @param int $uid Record uid, used as recuid and ref_uid
7189
     * @param int $workspace Workspace the record lives in
7190
     */
7191
    public function registerReferenceIndexRowsForDrop(string $table, int $uid, int $workspace): void
7192
    {
7193
        $this->referenceIndexUpdater->registerForDrop($table, $uid, $workspace);
7194
    }
7195
7196
    /*********************************************
7197
     *
7198
     * Misc functions
7199
     *
7200
     ********************************************/
7201
    /**
7202
     * Returning sorting number for tables with a "sortby" column
7203
     * Using when new records are created and existing records are moved around.
7204
     *
7205
     * The strategy is:
7206
     *  - if no record exists: set interval as sorting number
7207
     *  - if inserted before an element: put in the middle of the existing elements
7208
     *  - if inserted behind the last element: add interval to last sorting number
7209
     *  - if collision: move all subsequent records by 2 * interval, insert new record with collision + interval
7210
     *
7211
     * How to calculate the maximum possible inserts for the worst case of adding all records to the top,
7212
     * such that the sorting number stays within INT_MAX
7213
     *
7214
     * i = interval (currently 256)
7215
     * c = number of inserts until collision
7216
     * s = max sorting number to reach (INT_MAX - 32bit)
7217
     * n = number of records (~83 million)
7218
     *
7219
     * c = 2 * g
7220
     * g = log2(i) / 2 + 1
7221
     * n = g * s / i - g + 1
7222
     *
7223
     * The algorithm can be tuned by adjusting the interval value.
7224
     * Higher value means less collisions, but also less inserts are possible to stay within INT_MAX.
7225
     *
7226
     * @param string $table Table name
7227
     * @param int $uid Uid of record to find sorting number for. May be zero in case of new.
7228
     * @param int $pid Positioning PID, either >=0 (pointing to page in which case we find sorting number for first record in page) or <0 (pointing to record in which case to find next sorting number after this record)
7229
     * @return int|array|bool|null Returns integer if PID is >=0, otherwise an array with PID and sorting number. Possibly FALSE in case of error.
7230
     * @internal should only be used from within DataHandler
7231
     */
7232
    public function getSortNumber($table, $uid, $pid)
7233
    {
7234
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7235
        if (!$sortColumn) {
7236
            return null;
7237
        }
7238
7239
        $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($table);
7240
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
7241
        $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7242
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7243
7244
        $queryBuilder
7245
            ->select($sortColumn, 'pid', 'uid')
7246
            ->from($table);
7247
        if ($considerWorkspaces) {
7248
            $queryBuilder->addSelect('t3ver_state');
7249
        }
7250
7251
        // find and return the sorting value for the first record on that pid
7252
        if ($pid >= 0) {
7253
            // Fetches the first record (lowest sorting) under this pid
7254
            $queryBuilder
7255
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)));
7256
7257
            if ($considerWorkspaces) {
7258
                $queryBuilder->andWhere(
7259
                    $queryBuilder->expr()->orX(
7260
                        $queryBuilder->expr()->eq('t3ver_oid', 0),
7261
                        $queryBuilder->expr()->eq('t3ver_state', VersionState::MOVE_POINTER)
7262
                    )
7263
                );
7264
            }
7265
            $row = $queryBuilder
7266
                ->orderBy($sortColumn, 'ASC')
7267
                ->addOrderBy('uid', 'ASC')
7268
                ->setMaxResults(1)
7269
                ->execute()
7270
                ->fetch();
7271
7272
            if (!empty($row)) {
7273
                // The top record was the record itself, so we return its current sorting value
7274
                if ($row['uid'] == $uid) {
7275
                    return $row[$sortColumn];
7276
                }
7277
                // If the record sorting value < 1 we must resort all the records under this pid
7278
                if ($row[$sortColumn] < 1) {
7279
                    $this->increaseSortingOfFollowingRecords($table, (int)$pid);
7280
                    // Lowest sorting value after full resorting is $sortIntervals
7281
                    return $this->sortIntervals;
7282
                }
7283
                // Sorting number between current top element and zero
7284
                return floor($row[$sortColumn] / 2);
7285
            }
7286
            // No records, so we choose the default value as sorting-number
7287
            return $this->sortIntervals;
7288
        }
7289
7290
        // Find and return first possible sorting value AFTER record with given uid ($pid)
7291
        // Fetches the record which is supposed to be the prev record
7292
        $row = $queryBuilder
7293
                ->where($queryBuilder->expr()->eq(
7294
                    'uid',
7295
                    $queryBuilder->createNamedParameter(abs($pid), \PDO::PARAM_INT)
7296
                ))
7297
                ->execute()
7298
                ->fetch();
7299
7300
        // There is a previous record
7301
        if (!empty($row)) {
7302
            // Look if the record UID happens to be a versioned record. If so, find its live version.
7303
            // If this is already a moved record in workspace, this is not needed
7304
            if ((int)$row['t3ver_state'] !== VersionState::MOVE_POINTER && $lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortColumn . ',pid,uid')) {
7305
                $row = $lookForLiveVersion;
7306
            } elseif ($considerWorkspaces && $this->BE_USER->workspace > 0) {
7307
                // In case the previous record is moved in the workspace, we need to fetch the information from this specific record
7308
                $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $row['uid'], $sortColumn . ',pid,uid,t3ver_state');
7309
                if (is_array($versionedRecord) && (int)$versionedRecord['t3ver_state'] === VersionState::MOVE_POINTER) {
7310
                    $row = $versionedRecord;
7311
                }
7312
            }
7313
            // If the record should be inserted after itself, keep the current sorting information:
7314
            if ((int)$row['uid'] === (int)$uid) {
7315
                $sortNumber = $row[$sortColumn];
7316
            } else {
7317
                $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7318
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7319
7320
                $queryBuilder
7321
                        ->select($sortColumn, 'pid', 'uid')
7322
                        ->from($table)
7323
                        ->where(
7324
                            $queryBuilder->expr()->eq(
7325
                                'pid',
7326
                                $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT)
7327
                            ),
7328
                            $queryBuilder->expr()->gte(
7329
                                $sortColumn,
7330
                                $queryBuilder->createNamedParameter($row[$sortColumn], \PDO::PARAM_INT)
7331
                            )
7332
                        )
7333
                        ->orderBy($sortColumn, 'ASC')
7334
                        ->addOrderBy('uid', 'DESC')
7335
                        ->setMaxResults(2);
7336
7337
                if ($considerWorkspaces) {
7338
                    $queryBuilder->andWhere(
7339
                        $queryBuilder->expr()->orX(
7340
                            $queryBuilder->expr()->eq('t3ver_oid', 0),
7341
                            $queryBuilder->expr()->eq('t3ver_state', VersionState::MOVE_POINTER)
7342
                        )
7343
                    );
7344
                }
7345
7346
                $subResults = $queryBuilder
7347
                    ->execute()
7348
                    ->fetchAll();
7349
                // Fetches the next record in order to calculate the in-between sortNumber
7350
                // There was a record afterwards
7351
                if (count($subResults) === 2) {
7352
                    // There was a record afterwards, fetch that
7353
                    $subrow = array_pop($subResults);
7354
                    // The sortNumber is found in between these values
7355
                    $sortNumber = $row[$sortColumn] + floor(($subrow[$sortColumn] - $row[$sortColumn]) / 2);
7356
                    // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
7357
                    if ($sortNumber <= $row[$sortColumn] || $sortNumber >= $subrow[$sortColumn]) {
7358
                        $this->increaseSortingOfFollowingRecords($table, (int)$row['pid'], (int)$row[$sortColumn]);
7359
                        $sortNumber = $row[$sortColumn] + $this->sortIntervals;
7360
                    }
7361
                } else {
7362
                    // If after the last record in the list, we just add the sortInterval to the last sortvalue
7363
                    $sortNumber = $row[$sortColumn] + $this->sortIntervals;
7364
                }
7365
            }
7366
            return ['pid' => $row['pid'], 'sortNumber' => $sortNumber];
7367
        }
7368
        if ($this->enableLogging) {
7369
            $propArr = $this->getRecordProperties($table, $uid);
7370
            // OK, don't insert $propArr['event_pid'] here...
7371
            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record \'%s\' (%s) to after a non-existing record (uid=%s)', 1, [$propArr['header'], $table . ':' . $uid, abs($pid)], $propArr['pid']);
7372
        }
7373
        // There MUST be a previous record or else this cannot work
7374
        return false;
7375
    }
7376
7377
    /**
7378
     * Increases sorting field value of all records with sorting higher than $sortingValue
7379
     *
7380
     * Used internally by getSortNumber() to "make space" in sorting values when inserting new record
7381
     *
7382
     * @param string $table Table name
7383
     * @param int $pid Page Uid in which to resort records
7384
     * @param int $sortingValue All sorting numbers larger than this number will be shifted
7385
     * @see getSortNumber()
7386
     */
7387
    protected function increaseSortingOfFollowingRecords(string $table, int $pid, int $sortingValue = null): void
7388
    {
7389
        $sortBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7390
        if ($sortBy) {
7391
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7392
7393
            $queryBuilder
7394
                ->update($table)
7395
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7396
                ->set($sortBy, $queryBuilder->quoteIdentifier($sortBy) . ' + ' . $this->sortIntervals . ' + ' . $this->sortIntervals, false);
7397
            if ($sortingValue !== null) {
7398
                $queryBuilder->andWhere($queryBuilder->expr()->gt($sortBy, $sortingValue));
7399
            }
7400
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
7401
                $queryBuilder
7402
                    ->andWhere(
7403
                        $queryBuilder->expr()->eq('t3ver_oid', 0)
7404
                    );
7405
            }
7406
7407
            $deleteColumn = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? '';
7408
            if ($deleteColumn) {
7409
                $queryBuilder->andWhere($queryBuilder->expr()->eq($deleteColumn, 0));
7410
            }
7411
7412
            $queryBuilder->execute();
7413
        }
7414
    }
7415
7416
    /**
7417
     * Returning uid of previous localized record, if any, for tables with a "sortby" column
7418
     * Used when new localized records are created so that localized records are sorted in the same order as the default language records
7419
     *
7420
     * For a given record (A) uid (record we're translating) it finds first default language record (from the same colpos)
7421
     * with sorting smaller than given record (B).
7422
     * Then it fetches a translated version of record B and returns it's uid.
7423
     *
7424
     * If there is no record B, or it has no translation in given language, the record A uid is returned.
7425
     * The localized record will be placed the after record which uid is returned.
7426
     *
7427
     * @param string $table Table name
7428
     * @param int $uid Uid of default language record
7429
     * @param int $pid Pid of default language record
7430
     * @param int $language Language of localization
7431
     * @return int uid of record after which the localized record should be inserted
7432
     */
7433
    protected function getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
7434
    {
7435
        $previousLocalizedRecordUid = $uid;
7436
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7437
        if ($sortColumn) {
7438
            $select = [$sortColumn, 'pid', 'uid'];
7439
            // For content elements, we also need the colPos
7440
            if ($table === 'tt_content') {
7441
                $select[] = 'colPos';
7442
            }
7443
            // Get the sort value of the default language record
7444
            $row = BackendUtility::getRecord($table, $uid, implode(',', $select));
7445
            if (is_array($row)) {
7446
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7447
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7448
7449
                $queryBuilder
7450
                    ->select(...$select)
7451
                    ->from($table)
7452
                    ->where(
7453
                        $queryBuilder->expr()->eq(
7454
                            'pid',
7455
                            $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
7456
                        ),
7457
                        $queryBuilder->expr()->eq(
7458
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
7459
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
7460
                        ),
7461
                        $queryBuilder->expr()->lt(
7462
                            $sortColumn,
7463
                            $queryBuilder->createNamedParameter($row[$sortColumn], \PDO::PARAM_INT)
7464
                        )
7465
                    )
7466
                    ->orderBy($sortColumn, 'DESC')
7467
                    ->addOrderBy('uid', 'DESC')
7468
                    ->setMaxResults(1);
7469
                if ($table === 'tt_content') {
7470
                    $queryBuilder
7471
                        ->andWhere(
7472
                            $queryBuilder->expr()->eq(
7473
                                'colPos',
7474
                                $queryBuilder->createNamedParameter($row['colPos'], \PDO::PARAM_INT)
7475
                            )
7476
                        );
7477
                }
7478
                // If there is an element, find its localized record in specified localization language
7479
                if ($previousRow = $queryBuilder->execute()->fetch()) {
7480
                    $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $language);
7481
                    if (is_array($previousLocalizedRecord[0])) {
7482
                        $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
7483
                    }
7484
                }
7485
            }
7486
        }
7487
        return $previousLocalizedRecordUid;
7488
    }
7489
7490
    /**
7491
     * Returns a fieldArray with default values. Values will be picked up from the TCA array looking at the config key "default" for each column. If values are set in ->defaultValues they will overrule though.
7492
     * Used for new records and during copy operations for defaults
7493
     *
7494
     * @param string $table Table name for which to set default values.
7495
     * @return array Array with default values.
7496
     * @internal should only be used from within DataHandler
7497
     */
7498
    public function newFieldArray($table)
7499
    {
7500
        $fieldArray = [];
7501
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
7502
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $content) {
7503
                if (isset($this->defaultValues[$table][$field])) {
7504
                    $fieldArray[$field] = $this->defaultValues[$table][$field];
7505
                } elseif (isset($content['config']['default'])) {
7506
                    $fieldArray[$field] = $content['config']['default'];
7507
                }
7508
            }
7509
        }
7510
        return $fieldArray;
7511
    }
7512
7513
    /**
7514
     * If a "languageField" is specified for $table this function will add a possible value to the incoming array if none is found in there already.
7515
     *
7516
     * @param string $table Table name
7517
     * @param array $incomingFieldArray Incoming array (passed by reference)
7518
     * @internal should only be used from within DataHandler
7519
     */
7520
    public function addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
7521
    {
7522
        // Checking languages:
7523
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
7524
            if (!isset($incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
7525
                // Language field must be found in input row - otherwise it does not make sense.
7526
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
7527
                    ->getQueryBuilderForTable('sys_language');
7528
                $queryBuilder->getRestrictions()
7529
                    ->removeAll()
7530
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7531
                $queryBuilder
7532
                    ->select('uid')
7533
                    ->from('sys_language')
7534
                    ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
7535
                $rows = array_merge([['uid' => 0]], $queryBuilder->execute()->fetchAll(), [['uid' => -1]]);
7536
                foreach ($rows as $r) {
7537
                    if ($this->BE_USER->checkLanguageAccess($r['uid'])) {
7538
                        $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $r['uid'];
7539
                        break;
7540
                    }
7541
                }
7542
            }
7543
        }
7544
    }
7545
7546
    /**
7547
     * Returns the $data array from $table overridden in the fields defined in ->overrideValues.
7548
     *
7549
     * @param string $table Table name
7550
     * @param array $data Data array with fields from table. These will be overlaid with values in $this->overrideValues[$table]
7551
     * @return array Data array, processed.
7552
     * @internal should only be used from within DataHandler
7553
     */
7554
    public function overrideFieldArray($table, $data)
7555
    {
7556
        if (is_array($this->overrideValues[$table])) {
7557
            $data = array_merge($data, $this->overrideValues[$table]);
7558
        }
7559
        return $data;
7560
    }
7561
7562
    /**
7563
     * Compares the incoming field array with the current record and unsets all fields which are the same.
7564
     * Used for existing records being updated
7565
     *
7566
     * @param string $table Record table name
7567
     * @param int $id Record uid
7568
     * @param array $fieldArray Array of field=>value pairs intended to be inserted into the database. All keys with values matching exactly the current value will be unset!
7569
     * @return array Returns $fieldArray. If the returned array is empty, then the record should not be updated!
7570
     * @internal should only be used from within DataHandler
7571
     */
7572
    public function compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
7573
    {
7574
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7575
        $queryBuilder = $connection->createQueryBuilder();
7576
        $queryBuilder->getRestrictions()->removeAll();
7577
        $currentRecord = $queryBuilder->select('*')
7578
            ->from($table)
7579
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7580
            ->execute()
7581
            ->fetch();
7582
        // If the current record exists (which it should...), begin comparison:
7583
        if (is_array($currentRecord)) {
7584
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
7585
            $columnRecordTypes = [];
7586
            foreach ($currentRecord as $columnName => $_) {
7587
                $columnRecordTypes[$columnName] = '';
7588
                $type = $tableDetails->getColumn($columnName)->getType();
7589
                if ($type instanceof IntegerType) {
7590
                    $columnRecordTypes[$columnName] = 'int';
7591
                }
7592
            }
7593
            // Unset the fields which are similar:
7594
            foreach ($fieldArray as $col => $val) {
7595
                $fieldConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
7596
                $isNullField = (!empty($fieldConfiguration['eval']) && GeneralUtility::inList($fieldConfiguration['eval'], 'null'));
7597
7598
                // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
7599
                // In general this avoids to store superfluous data which also will be visualized in the editing history.
7600
                if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $columnRecordTypes[$col], $isNullField)) {
7601
                    unset($fieldArray[$col]);
7602
                } else {
7603
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
7604
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
7605
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
7606
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
7607
                    }
7608
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
7609
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
7610
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
7611
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
7612
                    }
7613
                }
7614
            }
7615
        } else {
7616
            // If the current record does not exist this is an error anyways and we just return an empty array here.
7617
            $fieldArray = [];
7618
        }
7619
        return $fieldArray;
7620
    }
7621
7622
    /**
7623
     * Determines whether submitted values and stored values are equal.
7624
     * This prevents from adding superfluous field changes which would be shown in the record history as well.
7625
     * For NULL fields (see accordant TCA definition 'eval' = 'null'), a special handling is required since
7626
     * (!strcmp(NULL, '')) would be a false-positive.
7627
     *
7628
     * @param mixed $submittedValue Value that has submitted (e.g. from a backend form)
7629
     * @param mixed $storedValue Value that is currently stored in the database
7630
     * @param string $storedType SQL type of the stored value column (see mysql_field_type(), e.g 'int', 'string',  ...)
7631
     * @param bool $allowNull Whether NULL values are allowed by accordant TCA definition ('eval' = 'null')
7632
     * @return bool Whether both values are considered to be equal
7633
     */
7634
    protected function isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
7635
    {
7636
        // No NULL values are allowed, this is the regular behaviour.
7637
        // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
7638
        if (!$allowNull) {
7639
            $result = (string)$submittedValue === (string)$storedValue || $storedType === 'int' && (int)$storedValue === (int)$submittedValue;
7640
        // Null values are allowed, but currently there's a real (not NULL) value.
7641
        // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
7642
        } elseif ($storedValue !== null) {
7643
            $result = (
7644
                $submittedValue !== null
7645
                && $this->isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
7646
            );
7647
        // Null values are allowed, and currently there's a NULL value.
7648
        // Thus, check whether a NULL value was submitted.
7649
        } else {
7650
            $result = ($submittedValue === null);
7651
        }
7652
7653
        return $result;
7654
    }
7655
7656
    /**
7657
     * Converts a HTML entity (like &#123;) to the character '123'
7658
     *
7659
     * @param string $input Input string
7660
     * @return string Output string
7661
     * @internal should only be used from within DataHandler
7662
     */
7663
    public function convNumEntityToByteValue($input)
7664
    {
7665
        $token = md5(microtime());
7666
        $parts = explode($token, preg_replace('/(&#([0-9]+);)/', $token . '\\2' . $token, $input));
7667
        foreach ($parts as $k => $v) {
7668
            if ($k % 2) {
7669
                $v = (int)$v;
7670
                // Just to make sure that control bytes are not converted.
7671
                if ($v > 32) {
7672
                    $parts[$k] = chr($v);
7673
                }
7674
            }
7675
        }
7676
        return implode('', $parts);
7677
    }
7678
7679
    /**
7680
     * Disables the delete clause for fetching records.
7681
     * In general only undeleted records will be used. If the delete
7682
     * clause is disabled, also deleted records are taken into account.
7683
     */
7684
    public function disableDeleteClause()
7685
    {
7686
        $this->disableDeleteClause = true;
7687
    }
7688
7689
    /**
7690
     * Returns delete-clause for the $table
7691
     *
7692
     * @param string $table Table name
7693
     * @return string Delete clause
7694
     * @internal should only be used from within DataHandler
7695
     */
7696
    public function deleteClause($table)
7697
    {
7698
        // Returns the proper delete-clause if any for a table from TCA
7699
        if (!$this->disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
7700
            return ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
7701
        }
7702
        return '';
7703
    }
7704
7705
    /**
7706
     * Add delete restriction if not disabled
7707
     *
7708
     * @param QueryRestrictionContainerInterface $restrictions
7709
     */
7710
    protected function addDeleteRestriction(QueryRestrictionContainerInterface $restrictions)
7711
    {
7712
        if (!$this->disableDeleteClause) {
7713
            $restrictions->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7714
        }
7715
    }
7716
7717
    /**
7718
     * Gets UID of parent record. If record is deleted it will be looked up in
7719
     * an array built before the record was deleted
7720
     *
7721
     * @param string $table Table where record lives/lived
7722
     * @param int $uid Record UID
7723
     * @return int[] Parent UIDs
7724
     */
7725
    protected function getOriginalParentOfRecord($table, $uid)
7726
    {
7727
        if (isset(self::$recordPidsForDeletedRecords[$table][$uid])) {
7728
            return self::$recordPidsForDeletedRecords[$table][$uid];
7729
        }
7730
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7730
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
7731
        return [$parentUid];
7732
    }
7733
7734
    /**
7735
     * Extract entries from TSconfig for a specific table. This will merge specific and default configuration together.
7736
     *
7737
     * @param string $table Table name
7738
     * @param array $TSconfig TSconfig for page
7739
     * @return array TSconfig merged
7740
     * @internal should only be used from within DataHandler
7741
     */
7742
    public function getTableEntries($table, $TSconfig)
7743
    {
7744
        $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : [];
7745
        $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : [];
7746
        ArrayUtility::mergeRecursiveWithOverrule($dA, $tA);
7747
        return $dA;
7748
    }
7749
7750
    /**
7751
     * Returns the pid of a record from $table with $uid
7752
     *
7753
     * @param string $table Table name
7754
     * @param int $uid Record uid
7755
     * @return int|false PID value (unless the record did not exist in which case FALSE is returned)
7756
     * @internal should only be used from within DataHandler
7757
     */
7758
    public function getPID($table, $uid)
7759
    {
7760
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7761
        $queryBuilder->getRestrictions()
7762
            ->removeAll();
7763
        $queryBuilder->select('pid')
7764
            ->from($table)
7765
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
7766
        if ($row = $queryBuilder->execute()->fetch()) {
7767
            return $row['pid'];
7768
        }
7769
        return false;
7770
    }
7771
7772
    /**
7773
     * Executing dbAnalysisStore
7774
     * This will save MM relations for new records but is executed after records are created because we need to know the ID of them
7775
     * @internal should only be used from within DataHandler
7776
     */
7777
    public function dbAnalysisStoreExec()
7778
    {
7779
        foreach ($this->dbAnalysisStore as $action) {
7780
            $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
7781
            if ($id) {
7782
                $action[0]->writeMM($action[1], $id, $action[3]);
7783
            }
7784
        }
7785
    }
7786
7787
    /**
7788
     * Returns array, $CPtable, of pages under the $pid going down to $counter levels.
7789
     * Selecting ONLY pages which the user has read-access to!
7790
     *
7791
     * @param array $CPtable Accumulation of page uid=>pid pairs in branch of $pid
7792
     * @param int $pid Page ID for which to find subpages
7793
     * @param int $counter Number of levels to go down.
7794
     * @param int $rootID ID of root point for new copied branch: The idea seems to be that a copy is not made of the already new page!
7795
     * @return array Return array.
7796
     * @internal should only be used from within DataHandler
7797
     */
7798
    public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
7799
    {
7800
        if ($counter) {
7801
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
7802
            $restrictions = $queryBuilder->getRestrictions()->removeAll();
7803
            $this->addDeleteRestriction($restrictions);
7804
            $queryBuilder
7805
                ->select('uid')
7806
                ->from('pages')
7807
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7808
                ->orderBy('sorting', 'DESC');
7809
            if (!$this->admin) {
7810
                $queryBuilder->andWhere($this->BE_USER->getPagePermsClause(Permission::PAGE_SHOW));
7811
            }
7812
            if ((int)$this->BE_USER->workspace === 0) {
7813
                $queryBuilder->andWhere(
7814
                    $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
7815
                );
7816
            } else {
7817
                $queryBuilder->andWhere($queryBuilder->expr()->in(
7818
                    't3ver_wsid',
7819
                    $queryBuilder->createNamedParameter([0, $this->BE_USER->workspace], Connection::PARAM_INT_ARRAY)
7820
                ));
7821
            }
7822
            $result = $queryBuilder->execute();
7823
7824
            $pages = [];
7825
            while ($row = $result->fetch()) {
7826
                $pages[$row['uid']] = $row;
7827
            }
7828
7829
            // Resolve placeholders of workspace versions
7830
            if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
7831
                $pages = array_reverse(
7832
                    $this->resolveVersionedRecords(
7833
                        'pages',
7834
                        'uid',
7835
                        'sorting',
7836
                        array_keys($pages)
7837
                    ),
7838
                    true
7839
                );
7840
            }
7841
7842
            foreach ($pages as $page) {
7843
                if ($page['uid'] != $rootID) {
7844
                    $CPtable[$page['uid']] = $pid;
7845
                    // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
7846
                    if ($counter - 1) {
7847
                        $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
7848
                    }
7849
                }
7850
            }
7851
        }
7852
        return $CPtable;
7853
    }
7854
7855
    /**
7856
     * List of all tables (those administrators has access to = array_keys of $GLOBALS['TCA'])
7857
     *
7858
     * @return array Array of all TCA table names
7859
     * @internal should only be used from within DataHandler
7860
     */
7861
    public function compileAdminTables()
7862
    {
7863
        return array_keys($GLOBALS['TCA']);
7864
    }
7865
7866
    /**
7867
     * Checks if any uniqueInPid eval input fields are in the record and if so, they are re-written to be correct.
7868
     *
7869
     * @param string $table Table name
7870
     * @param int $uid Record UID
7871
     * @internal should only be used from within DataHandler
7872
     */
7873
    public function fixUniqueInPid($table, $uid)
7874
    {
7875
        if (empty($GLOBALS['TCA'][$table])) {
7876
            return;
7877
        }
7878
7879
        $curData = $this->recordInfo($table, $uid, '*');
7880
        $newData = [];
7881
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7882
            if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
7883
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7884
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
7885
                    $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
7886
                    if ((string)$newV !== (string)$curData[$field]) {
7887
                        $newData[$field] = $newV;
7888
                    }
7889
                }
7890
            }
7891
        }
7892
        // IF there are changed fields, then update the database
7893
        if (!empty($newData)) {
7894
            $this->updateDB($table, $uid, $newData);
7895
        }
7896
    }
7897
7898
    /**
7899
     * Checks if any uniqueInSite eval fields are in the record and if so, they are re-written to be correct.
7900
     *
7901
     * @param string $table Table name
7902
     * @param int $uid Record UID
7903
     * @return bool whether the record had to be fixed or not
7904
     */
7905
    protected function fixUniqueInSite(string $table, int $uid): bool
7906
    {
7907
        $curData = $this->recordInfo($table, $uid, '*');
7908
        $workspaceId = $this->BE_USER->workspace;
7909
        $newData = [];
7910
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7911
            if ($conf['config']['type'] === 'slug' && (string)$curData[$field] !== '') {
7912
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7913
                if (in_array('uniqueInSite', $evalCodesArray, true)) {
7914
                    $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $conf['config'], $workspaceId);
7915
                    $state = RecordStateFactory::forName($table)->fromArray($curData);
0 ignored issues
show
Bug introduced by
It seems like $curData can also be of type null; however, parameter $data of TYPO3\CMS\Core\DataHandl...ateFactory::fromArray() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7915
                    $state = RecordStateFactory::forName($table)->fromArray(/** @scrutinizer ignore-type */ $curData);
Loading history...
7916
                    $newValue = $helper->buildSlugForUniqueInSite($curData[$field], $state);
7917
                    if ((string)$newValue !== (string)$curData[$field]) {
7918
                        $newData[$field] = $newValue;
7919
                    }
7920
                }
7921
            }
7922
        }
7923
        // IF there are changed fields, then update the database
7924
        if (!empty($newData)) {
7925
            $this->updateDB($table, $uid, $newData);
7926
            return true;
7927
        }
7928
        return false;
7929
    }
7930
7931
    /**
7932
     * Check if there are subpages that need an adoption as well
7933
     * @param int $pageId
7934
     */
7935
    protected function fixUniqueInSiteForSubpages(int $pageId)
7936
    {
7937
        // Get ALL subpages to update - read-permissions are respected
7938
        $subPages = $this->int_pageTreeInfo([], $pageId, 99, $pageId);
7939
        // Now fix uniqueInSite for subpages
7940
        foreach ($subPages as $thePageUid => $thePagePid) {
7941
            $recordWasModified = $this->fixUniqueInSite('pages', $thePageUid);
7942
            if ($recordWasModified) {
7943
                // @todo: Add logging and history - but how? we don't know the data that was in the system before
7944
            }
7945
        }
7946
    }
7947
7948
    /**
7949
     * When er record is copied you can specify fields from the previous record which should be copied into the new one
7950
     * This function is also called with new elements. But then $update must be set to zero and $newData containing the data array. In that case data in the incoming array is NOT overridden. (250202)
7951
     *
7952
     * @param string $table Table name
7953
     * @param int $uid Record UID
7954
     * @param int $prevUid UID of previous record
7955
     * @param bool $update If set, updates the record
7956
     * @param array $newData Input array. If fields are already specified AND $update is not set, values are not set in output array.
7957
     * @return array Output array (For when the copying operation needs to get the information instead of updating the info)
7958
     * @internal should only be used from within DataHandler
7959
     */
7960
    public function fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData = [])
7961
    {
7962
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
7963
            $prevData = $this->recordInfo($table, $prevUid, '*');
7964
            $theFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
7965
            foreach ($theFields as $field) {
7966
                if ($GLOBALS['TCA'][$table]['columns'][$field] && ($update || !isset($newData[$field]))) {
7967
                    $newData[$field] = $prevData[$field];
7968
                }
7969
            }
7970
            if ($update && !empty($newData)) {
7971
                $this->updateDB($table, $uid, $newData);
7972
            }
7973
        }
7974
        return $newData;
7975
    }
7976
7977
    /**
7978
     * Casts a reference value. In case MM relations or foreign_field
7979
     * references are used. All other configurations, as well as
7980
     * foreign_table(!) could be stored as comma-separated-values
7981
     * as well. Since the system is not able to determine the default
7982
     * value automatically then, the TCA default value is used if
7983
     * it has been defined.
7984
     *
7985
     * @param int|string $value The value to be casted (e.g. '', '0', '1,2,3')
7986
     * @param array $configuration The TCA configuration of the accordant field
7987
     * @return int|string
7988
     */
7989
    protected function castReferenceValue($value, array $configuration)
7990
    {
7991
        if ((string)$value !== '') {
7992
            return $value;
7993
        }
7994
7995
        if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
7996
            return 0;
7997
        }
7998
7999
        if (array_key_exists('default', $configuration)) {
8000
            return $configuration['default'];
8001
        }
8002
8003
        return $value;
8004
    }
8005
8006
    /**
8007
     * Returns TRUE if the TCA/columns field type is a DB reference field
8008
     *
8009
     * @param array $conf Config array for TCA/columns field
8010
     * @return bool TRUE if DB reference field (group/db or select with foreign-table)
8011
     * @internal should only be used from within DataHandler
8012
     */
8013
    public function isReferenceField($conf)
8014
    {
8015
        return $conf['type'] === 'group' && $conf['internal_type'] === 'db' || $conf['type'] === 'select' && $conf['foreign_table'];
8016
    }
8017
8018
    /**
8019
     * Returns the subtype as a string of an inline field.
8020
     * If it's not an inline field at all, it returns FALSE.
8021
     *
8022
     * @param array $conf Config array for TCA/columns field
8023
     * @return string|bool string Inline subtype (field|mm|list), boolean: FALSE
8024
     * @internal should only be used from within DataHandler
8025
     */
8026
    public function getInlineFieldType($conf)
8027
    {
8028
        if ($conf['type'] !== 'inline' || !$conf['foreign_table']) {
8029
            return false;
8030
        }
8031
        if ($conf['foreign_field']) {
8032
            // The reference to the parent is stored in a pointer field in the child record
8033
            return 'field';
8034
        }
8035
        if ($conf['MM']) {
8036
            // Regular MM intermediate table is used to store data
8037
            return 'mm';
8038
        }
8039
        // An item list (separated by comma) is stored (like select type is doing)
8040
        return 'list';
8041
    }
8042
8043
    /**
8044
     * Get modified header for a copied record
8045
     *
8046
     * @param string $table Table name
8047
     * @param int $pid PID value in which other records to test might be
8048
     * @param string $field Field name to get header value for.
8049
     * @param string $value Current field value
8050
     * @param int $count Counter (number of recursions)
8051
     * @param string $prevTitle Previous title we checked for (in previous recursion)
8052
     * @return string The field value, possibly appended with a "copy label
8053
     * @internal should only be used from within DataHandler
8054
     */
8055
    public function getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
8056
    {
8057
        // Set title value to check for:
8058
        $checkTitle = $value;
8059
        if ($count > 0) {
8060
            $checkTitle = $value . rtrim(' ' . sprintf($this->prependLabel($table), $count));
8061
        }
8062
        // Do check:
8063
        if ($prevTitle != $checkTitle || $count < 100) {
8064
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8065
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
8066
            $rowCount = $queryBuilder
8067
                ->count('uid')
8068
                ->from($table)
8069
                ->where(
8070
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
8071
                    $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($checkTitle, \PDO::PARAM_STR))
8072
                )
8073
                ->execute()
8074
                ->fetchColumn(0);
8075
            if ($rowCount) {
8076
                return $this->getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
8077
            }
8078
        }
8079
        // Default is to just return the current input title if no other was returned before:
8080
        return $checkTitle;
8081
    }
8082
8083
    /**
8084
     * Return "copy" label for a table. Although the name is "prepend" it actually APPENDs the label (after ...)
8085
     *
8086
     * @param string $table Table name
8087
     * @return string Label to append, containing "%s" for the number
8088
     * @see getCopyHeader()
8089
     * @internal should only be used from within DataHandler
8090
     */
8091
    public function prependLabel($table)
8092
    {
8093
        return $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
8094
    }
8095
8096
    /**
8097
     * Get the final pid based on $table and $pid ($destPid type... pos/neg)
8098
     *
8099
     * @param string $table Table name
8100
     * @param int $pid "Destination pid" : If the value is >= 0 it's just returned directly (through (int)though) but if the value is <0 then the method looks up the record with the uid equal to abs($pid) (positive number) and returns the PID of that record! The idea is that negative numbers point to the record AFTER WHICH the position is supposed to be!
8101
     * @return int
8102
     * @internal should only be used from within DataHandler
8103
     */
8104
    public function resolvePid($table, $pid)
8105
    {
8106
        $pid = (int)$pid;
8107
        if ($pid < 0) {
8108
            $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8109
            $query->getRestrictions()
8110
                ->removeAll();
8111
            $row = $query
8112
                ->select('pid')
8113
                ->from($table)
8114
                ->where($query->expr()->eq('uid', $query->createNamedParameter(abs($pid), \PDO::PARAM_INT)))
8115
                ->execute()
8116
                ->fetch();
8117
            $pid = (int)$row['pid'];
8118
        }
8119
        return $pid;
8120
    }
8121
8122
    /**
8123
     * Removes the prependAtCopy prefix on values
8124
     *
8125
     * @param string $table Table name
8126
     * @param string $value The value to fix
8127
     * @return string Clean name
8128
     * @internal should only be used from within DataHandler
8129
     */
8130
    public function clearPrefixFromValue($table, $value)
8131
    {
8132
        $regex = '/\s' . sprintf(preg_quote($this->prependLabel($table)), '[0-9]*') . '$/';
8133
        return @preg_replace($regex, '', $value);
8134
    }
8135
8136
    /**
8137
     * Check if there are records from tables on the pages to be deleted which the current user is not allowed to
8138
     *
8139
     * @param int[] $pageIds IDs of pages which should be checked
8140
     * @return string[]|null Return null, if permission granted, otherwise an array with the tables that are not allowed to be deleted
8141
     * @see canDeletePage()
8142
     */
8143
    protected function checkForRecordsFromDisallowedTables(array $pageIds): ?array
8144
    {
8145
        if ($this->admin) {
8146
            return null;
8147
        }
8148
8149
        $disallowedTables = [];
8150
        if (!empty($pageIds)) {
8151
            $tableNames = $this->compileAdminTables();
8152
            foreach ($tableNames as $table) {
8153
                $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8154
                $query->getRestrictions()
8155
                    ->removeAll()
8156
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8157
                $count = $query->count('uid')
8158
                    ->from($table)
8159
                    ->where($query->expr()->in(
8160
                        'pid',
8161
                        $query->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
8162
                    ))
8163
                    ->execute()
8164
                    ->fetchColumn(0);
8165
                if ($count && ($this->tableReadOnly($table) || !$this->checkModifyAccessList($table))) {
8166
                    $disallowedTables[] = $table;
8167
                }
8168
            }
8169
        }
8170
        return !empty($disallowedTables) ? $disallowedTables : null;
8171
    }
8172
8173
    /**
8174
     * Determine if a record was copied or if a record is the result of a copy action.
8175
     *
8176
     * @param string $table The tablename of the record
8177
     * @param int $uid The uid of the record
8178
     * @return bool Returns TRUE if the record is copied or is the result of a copy action
8179
     * @internal should only be used from within DataHandler
8180
     */
8181
    public function isRecordCopied($table, $uid)
8182
    {
8183
        // If the record was copied:
8184
        if (isset($this->copyMappingArray[$table][$uid])) {
8185
            return true;
8186
        }
8187
        if (isset($this->copyMappingArray[$table]) && in_array($uid, array_values($this->copyMappingArray[$table]))) {
8188
            return true;
8189
        }
8190
        return false;
8191
    }
8192
8193
    /******************************
8194
     *
8195
     * Clearing cache
8196
     *
8197
     ******************************/
8198
8199
    /**
8200
     * Clearing the cache based on a page being updated
8201
     * If the $table is 'pages' then cache is cleared for all pages on the same level (and subsequent?)
8202
     * Else just clear the cache for the parent page of the record.
8203
     *
8204
     * @param string $table Table name of record that was just updated.
8205
     * @param int $uid UID of updated / inserted record
8206
     * @param int $pid REAL PID of page of a deleted/moved record to get TSconfig in ClearCache.
8207
     * @internal This method is not meant to be called directly but only from the core itself or from hooks
8208
     */
8209
    public function registerRecordIdForPageCacheClearing($table, $uid, $pid = null)
8210
    {
8211
        if (!is_array(static::$recordsToClearCacheFor[$table])) {
8212
            static::$recordsToClearCacheFor[$table] = [];
8213
        }
8214
        static::$recordsToClearCacheFor[$table][] = (int)$uid;
8215
        if ($pid !== null) {
8216
            if (!is_array(static::$recordPidsForDeletedRecords[$table])) {
8217
                static::$recordPidsForDeletedRecords[$table] = [];
8218
            }
8219
            static::$recordPidsForDeletedRecords[$table][$uid][] = (int)$pid;
8220
        }
8221
    }
8222
8223
    /**
8224
     * Do the actual clear cache
8225
     */
8226
    protected function processClearCacheQueue()
8227
    {
8228
        $tagsToClear = [];
8229
        $clearCacheCommands = [];
8230
8231
        foreach (static::$recordsToClearCacheFor as $table => $uids) {
8232
            foreach (array_unique($uids) as $uid) {
8233
                if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
8234
                    return;
8235
                }
8236
                // For move commands we may get more then 1 parent.
8237
                $pageUids = $this->getOriginalParentOfRecord($table, $uid);
8238
                foreach ($pageUids as $originalParent) {
8239
                    [$tagsToClearFromPrepare, $clearCacheCommandsFromPrepare]
8240
                        = $this->prepareCacheFlush($table, $uid, $originalParent);
8241
                    $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
8242
                    $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
8243
                }
8244
            }
8245
        }
8246
8247
        /** @var CacheManager $cacheManager */
8248
        $cacheManager = $this->getCacheManager();
8249
        $cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear));
8250
8251
        // Filter duplicate cache commands from cacheQueue
8252
        $clearCacheCommands = array_unique($clearCacheCommands);
8253
        // Execute collected clear cache commands from page TSConfig
8254
        foreach ($clearCacheCommands as $command) {
8255
            $this->clear_cacheCmd($command);
8256
        }
8257
8258
        // Reset the cache clearing array
8259
        static::$recordsToClearCacheFor = [];
8260
8261
        // Reset the original pid array
8262
        static::$recordPidsForDeletedRecords = [];
8263
    }
8264
8265
    /**
8266
     * Prepare the cache clearing
8267
     *
8268
     * @param string $table Table name of record that needs to be cleared
8269
     * @param int $uid UID of record for which the cache needs to be cleared
8270
     * @param int $pid Original pid of the page of the record which the cache needs to be cleared
8271
     * @return array Array with tagsToClear and clearCacheCommands
8272
     * @internal This function is internal only it may be changed/removed also in minor version numbers.
8273
     */
8274
    protected function prepareCacheFlush($table, $uid, $pid)
8275
    {
8276
        $tagsToClear = [];
8277
        $clearCacheCommands = [];
8278
        $pageUid = 0;
8279
        // Get Page TSconfig relevant:
8280
        $TSConfig = BackendUtility::getPagesTSconfig($pid)['TCEMAIN.'] ?? [];
8281
        if (empty($TSConfig['clearCache_disable']) && $this->BE_USER->workspace === 0) {
8282
            $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
8283
            // If table is "pages":
8284
            $pageIdsThatNeedCacheFlush = [];
8285
            if ($table === 'pages') {
8286
                // Find out if the record is a get the original page
8287
                $pageUid = $this->getDefaultLanguagePageId($uid);
8288
8289
                // Builds list of pages on the SAME level as this page (siblings)
8290
                $queryBuilder = $connectionPool->getQueryBuilderForTable('pages');
8291
                $queryBuilder->getRestrictions()
8292
                    ->removeAll()
8293
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8294
                $siblings = $queryBuilder
8295
                    ->select('A.pid AS pid', 'B.uid AS uid')
8296
                    ->from('pages', 'A')
8297
                    ->from('pages', 'B')
8298
                    ->where(
8299
                        $queryBuilder->expr()->eq('A.uid', $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)),
8300
                        $queryBuilder->expr()->eq('B.pid', $queryBuilder->quoteIdentifier('A.pid')),
8301
                        $queryBuilder->expr()->gte('A.pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8302
                    )
8303
                    ->execute();
8304
8305
                $parentPageId = 0;
8306
                while ($row_tmp = $siblings->fetch()) {
8307
                    $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
8308
                    $parentPageId = (int)$row_tmp['pid'];
8309
                    // Add children as well:
8310
                    if ($TSConfig['clearCache_pageSiblingChildren']) {
8311
                        $siblingChildrenQuery = $connectionPool->getQueryBuilderForTable('pages');
8312
                        $siblingChildrenQuery->getRestrictions()
8313
                            ->removeAll()
8314
                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8315
                        $siblingChildren = $siblingChildrenQuery
8316
                            ->select('uid')
8317
                            ->from('pages')
8318
                            ->where($siblingChildrenQuery->expr()->eq(
8319
                                'pid',
8320
                                $siblingChildrenQuery->createNamedParameter($row_tmp['uid'], \PDO::PARAM_INT)
8321
                            ))
8322
                            ->execute();
8323
                        while ($row_tmp2 = $siblingChildren->fetch()) {
8324
                            $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
8325
                        }
8326
                    }
8327
                }
8328
                // Finally, add the parent page as well when clearing a specific page
8329
                if ($parentPageId > 0) {
8330
                    $pageIdsThatNeedCacheFlush[] = $parentPageId;
8331
                }
8332
                // Add grand-parent as well if configured
8333
                if ($TSConfig['clearCache_pageGrandParent']) {
8334
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8335
                    $parentQuery->getRestrictions()
8336
                        ->removeAll()
8337
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8338
                    $row_tmp = $parentQuery
8339
                        ->select('pid')
8340
                        ->from('pages')
8341
                        ->where($parentQuery->expr()->eq(
8342
                            'uid',
8343
                            $parentQuery->createNamedParameter($parentPageId, \PDO::PARAM_INT)
8344
                        ))
8345
                        ->execute()
8346
                        ->fetch();
8347
                    if (!empty($row_tmp)) {
8348
                        $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
8349
                    }
8350
                }
8351
            } else {
8352
                // For other tables than "pages", delete cache for the records "parent page".
8353
                $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
8354
                // Add the parent page as well
8355
                if ($TSConfig['clearCache_pageGrandParent']) {
8356
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8357
                    $parentQuery->getRestrictions()
8358
                        ->removeAll()
8359
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8360
                    $parentPageRecord = $parentQuery
8361
                        ->select('pid')
8362
                        ->from('pages')
8363
                        ->where($parentQuery->expr()->eq(
8364
                            'uid',
8365
                            $parentQuery->createNamedParameter($pageUid, \PDO::PARAM_INT)
8366
                        ))
8367
                        ->execute()
8368
                        ->fetch();
8369
                    if (!empty($parentPageRecord)) {
8370
                        $pageIdsThatNeedCacheFlush[] = (int)$parentPageRecord['pid'];
8371
                    }
8372
                }
8373
            }
8374
            // Call pre-processing function for clearing of cache for page ids:
8375
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8376
                $_params = ['pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()'];
8377
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8378
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8379
            }
8380
            // Delete cache for selected pages:
8381
            foreach ($pageIdsThatNeedCacheFlush as $pageId) {
8382
                $tagsToClear['pageId_' . $pageId] = true;
8383
            }
8384
            // Queue delete cache for current table and record
8385
            $tagsToClear[$table] = true;
8386
            $tagsToClear[$table . '_' . $uid] = true;
8387
        }
8388
        // Clear cache for pages entered in TSconfig:
8389
        if (!empty($TSConfig['clearCacheCmd'])) {
8390
            $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
8391
            $clearCacheCommands = array_unique($commands);
8392
        }
8393
        // Call post processing function for clear-cache:
8394
        $_params = ['table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig, 'tags' => $tagsToClear];
8395
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8396
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8397
        }
8398
        return [
8399
            $tagsToClear,
8400
            $clearCacheCommands
8401
        ];
8402
    }
8403
8404
    /**
8405
     * Clears the cache based on the command $cacheCmd.
8406
     *
8407
     * $cacheCmd='pages'
8408
     * Clears cache for all pages and page-based caches inside the cache manager.
8409
     * Requires admin-flag to be set for BE_USER.
8410
     *
8411
     * $cacheCmd='all'
8412
     * Clears all cache_tables. This is necessary if templates are updated.
8413
     * Requires admin-flag to be set for BE_USER.
8414
     *
8415
     * The following cache_* are intentionally not cleared by 'all'
8416
     *
8417
     * - imagesizes:	Clearing this table would cause a lot of unneeded
8418
     * Imagemagick calls because the size information has
8419
     * to be fetched again after clearing.
8420
     * - all caches inside the cache manager that are inside the group "system"
8421
     * - they are only needed to build up the core system and templates.
8422
     *   If the group of system caches needs to be deleted explicitly, use
8423
     *   flushCachesInGroup('system') of CacheManager directly.
8424
     *
8425
     * $cacheCmd=[integer]
8426
     * Clears cache for the page pointed to by $cacheCmd (an integer).
8427
     *
8428
     * $cacheCmd='cacheTag:[string]'
8429
     * Flush page and pagesection cache by given tag
8430
     *
8431
     * $cacheCmd='cacheId:[string]'
8432
     * Removes cache identifier from page and page section cache
8433
     *
8434
     * Can call a list of post processing functions as defined in
8435
     * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc']
8436
     * (numeric array with values being the function references, called by
8437
     * GeneralUtility::callUserFunction()).
8438
     *
8439
     *
8440
     * @param string $cacheCmd The cache command, see above description
8441
     */
8442
    public function clear_cacheCmd($cacheCmd)
8443
    {
8444
        if (is_object($this->BE_USER)) {
8445
            $this->BE_USER->writeLog(SystemLogType::CACHE, SystemLogCacheAction::CLEAR, SystemLogErrorClassification::MESSAGE, 0, 'User %s has cleared the cache (cacheCmd=%s)', [$this->BE_USER->user['username'], $cacheCmd]);
8446
        }
8447
        $userTsConfig = $this->BE_USER->getTSConfig();
8448
        switch (strtolower($cacheCmd)) {
8449
            case 'pages':
8450
                if ($this->admin || ($userTsConfig['options.']['clearCache.']['pages'] ?? false)) {
8451
                    $this->getCacheManager()->flushCachesInGroup('pages');
8452
                }
8453
                break;
8454
            case 'all':
8455
                // allow to clear all caches if the TS config option is enabled or the option is not explicitly
8456
                // disabled for admins (which could clear all caches by default). The latter option is useful
8457
                // for big production sites where it should be possible to restrict the cache clearing for some admins.
8458
                if (($userTsConfig['options.']['clearCache.']['all'] ?? false)
8459
                    || ($this->admin && (bool)($userTsConfig['options.']['clearCache.']['all'] ?? true))
8460
                ) {
8461
                    $this->getCacheManager()->flushCaches();
8462
                    GeneralUtility::makeInstance(ConnectionPool::class)
8463
                        ->getConnectionForTable('cache_treelist')
8464
                        ->truncate('cache_treelist');
8465
8466
                    // Delete Opcode Cache
8467
                    GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
8468
                }
8469
                break;
8470
        }
8471
8472
        $tagsToFlush = [];
8473
        // Clear cache for a page ID!
8474
        if (MathUtility::canBeInterpretedAsInteger($cacheCmd)) {
8475
            $list_cache = [$cacheCmd];
8476
            // Call pre-processing function for clearing of cache for page ids:
8477
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8478
                $_params = ['pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()'];
8479
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8480
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8481
            }
8482
            // Delete cache for selected pages:
8483
            if (is_array($list_cache)) {
0 ignored issues
show
introduced by
The condition is_array($list_cache) is always true.
Loading history...
8484
                foreach ($list_cache as $pageId) {
8485
                    $tagsToFlush[] = 'pageId_' . (int)$pageId;
8486
                }
8487
            }
8488
        }
8489
        // flush cache by tag
8490
        if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
8491
            $cacheTag = substr($cacheCmd, 9);
8492
            $tagsToFlush[] = $cacheTag;
8493
        }
8494
        // process caching framework operations
8495
        if (!empty($tagsToFlush)) {
8496
            $this->getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);
0 ignored issues
show
Bug introduced by
The method getCacheManager() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

8496
            $this->/** @scrutinizer ignore-call */ 
8497
                   getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
8497
        }
8498
8499
        // Call post processing function for clear-cache:
8500
        $_params = ['cacheCmd' => strtolower($cacheCmd), 'tags' => $tagsToFlush];
8501
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8502
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8503
        }
8504
    }
8505
8506
    /*****************************
8507
     *
8508
     * Logging
8509
     *
8510
     *****************************/
8511
    /**
8512
     * Logging actions from DataHandler
8513
     *
8514
     * @param string $table Table name the log entry is concerned with. Blank if NA
8515
     * @param int $recuid Record UID. Zero if NA
8516
     * @param int $action Action number: 0=No category, 1=new record, 2=update record, 3= delete record, 4= move record, 5= Check/evaluate
8517
     * @param int $recpid Normally 0 (zero). If set, it indicates that this log-entry is used to notify the backend of a record which is moved to another location
8518
     * @param int $error The severity: 0 = message, 1 = error, 2 = System Error, 3 = security notice (admin), 4 warning
8519
     * @param string $details Default error message in english
8520
     * @param int $details_nr This number is unique for every combination of $type and $action. This is the error-message number, which can later be used to translate error messages. 0 if not categorized, -1 if temporary
8521
     * @param array $data Array with special information that may go into $details by '%s' marks / sprintf() when the log is shown
8522
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
8523
     * @param string $NEWid NEW id for new records
8524
     * @return int Log entry UID (0 if no log entry was written or logging is disabled)
8525
     * @see \TYPO3\CMS\Core\SysLog\Action\Database for all available values of argument $action
8526
     * @see \TYPO3\CMS\Core\SysLog\Error for all available values of argument $error
8527
     * @internal should only be used from within TYPO3 Core
8528
     */
8529
    public function log($table, $recuid, $action, $recpid, $error, $details, $details_nr = -1, $data = [], $event_pid = -1, $NEWid = '')
8530
    {
8531
        if (!$this->enableLogging) {
8532
            return 0;
8533
        }
8534
        // Type value for DataHandler
8535
        if (!$this->storeLogMessages) {
8536
            $details = '';
8537
        }
8538
        if ($error > 0) {
8539
            $detailMessage = $details;
8540
            if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
8541
                $detailMessage = vsprintf($details, $data);
8542
            }
8543
            $this->errorLog[] = '[' . SystemLogType::DB . '.' . $action . '.' . $details_nr . ']: ' . $detailMessage;
8544
        }
8545
        return $this->BE_USER->writelog(SystemLogType::DB, $action, $error, $details_nr, $details, $data, $table, $recuid, $recpid, $event_pid, $NEWid);
8546
    }
8547
8548
    /**
8549
     * Simple logging function meant to be used when logging messages is not yet fixed.
8550
     *
8551
     * @param string $message Message string
8552
     * @param int $error Error code, see log()
8553
     * @return int Log entry UID
8554
     * @see log()
8555
     * @internal should only be used from within TYPO3 Core
8556
     */
8557
    public function newlog($message, $error = SystemLogErrorClassification::MESSAGE)
8558
    {
8559
        return $this->log('', 0, SystemLogGenericAction::UNDEFINED, 0, $error, $message, -1);
8560
    }
8561
8562
    /**
8563
     * Print log error messages from the operations of this script instance
8564
     * @internal should only be used from within TYPO3 Core
8565
     */
8566
    public function printLogErrorMessages()
8567
    {
8568
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
8569
        $queryBuilder->getRestrictions()->removeAll();
8570
        $result = $queryBuilder
8571
            ->select('*')
8572
            ->from('sys_log')
8573
            ->where(
8574
                $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
8575
                $queryBuilder->expr()->lt('action', $queryBuilder->createNamedParameter(256, \PDO::PARAM_INT)),
8576
                $queryBuilder->expr()->eq(
8577
                    'userid',
8578
                    $queryBuilder->createNamedParameter($this->BE_USER->user['uid'], \PDO::PARAM_INT)
8579
                ),
8580
                $queryBuilder->expr()->eq(
8581
                    'tstamp',
8582
                    $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
8583
                ),
8584
                $queryBuilder->expr()->neq('error', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8585
            )
8586
            ->execute();
8587
8588
        while ($row = $result->fetch()) {
8589
            $log_data = unserialize($row['log_data']);
8590
            $msg = $row['error'] . ': ' . sprintf($row['details'], $log_data[0], $log_data[1], $log_data[2], $log_data[3], $log_data[4]);
8591
            /** @var FlashMessage $flashMessage */
8592
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', $row['error'] === 4 ? FlashMessage::WARNING : FlashMessage::ERROR, true);
8593
            /** @var FlashMessageService $flashMessageService */
8594
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
8595
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
8596
            $defaultFlashMessageQueue->enqueue($flashMessage);
8597
        }
8598
    }
8599
8600
    /*****************************
8601
     *
8602
     * Internal (do not use outside Core!)
8603
     *
8604
     *****************************/
8605
8606
    /**
8607
     * Find out if the record is a get the original page
8608
     *
8609
     * @param int $pageId the page UID (can be the default page record, or a page translation record ID)
8610
     * @return int the page UID of the default page record
8611
     */
8612
    protected function getDefaultLanguagePageId(int $pageId): int
8613
    {
8614
        $localizationParentFieldName = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
8615
        $row = $this->recordInfo('pages', $pageId, $localizationParentFieldName);
8616
        $localizationParent = (int)$row[$localizationParentFieldName];
8617
        if ($localizationParent > 0) {
8618
            return $localizationParent;
8619
        }
8620
        return $pageId;
8621
    }
8622
8623
    /**
8624
     * Preprocesses field array based on field type. Some fields must be adjusted
8625
     * before going to database. This is done on the copy of the field array because
8626
     * original values are used in remap action later.
8627
     *
8628
     * @param string $table	Table name
8629
     * @param array $fieldArray	Field array to check
8630
     * @return array Updated field array
8631
     * @internal should only be used from within TYPO3 Core
8632
     */
8633
    public function insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
8634
    {
8635
        $result = $fieldArray;
8636
        foreach ($fieldArray as $field => $value) {
8637
            if (!MathUtility::canBeInterpretedAsInteger($value)
8638
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'inline'
8639
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_field']) {
8640
                $result[$field] = count(GeneralUtility::trimExplode(',', $value, true));
8641
            }
8642
        }
8643
        return $result;
8644
    }
8645
8646
    /**
8647
     * Determines whether a particular record has been deleted
8648
     * using DataHandler::deleteRecord() in this instance.
8649
     *
8650
     * @param string $tableName
8651
     * @param string $uid
8652
     * @return bool
8653
     * @internal should only be used from within TYPO3 Core
8654
     */
8655
    public function hasDeletedRecord($tableName, $uid)
8656
    {
8657
        return
8658
            !empty($this->deletedRecords[$tableName])
8659
            && in_array($uid, $this->deletedRecords[$tableName])
8660
        ;
8661
    }
8662
8663
    /**
8664
     * Gets the automatically versionized id of a record.
8665
     *
8666
     * @param string $table Name of the table
8667
     * @param int $id Uid of the record
8668
     * @return int|null
8669
     * @internal should only be used from within TYPO3 Core
8670
     */
8671
    public function getAutoVersionId($table, $id): ?int
8672
    {
8673
        $result = null;
8674
        if (isset($this->autoVersionIdMap[$table][$id])) {
8675
            $result = (int)trim($this->autoVersionIdMap[$table][$id]);
8676
        }
8677
        return $result;
8678
    }
8679
8680
    /**
8681
     * Overlays the automatically versionized id of a record.
8682
     *
8683
     * @param string $table Name of the table
8684
     * @param int $id Uid of the record
8685
     * @return int
8686
     */
8687
    protected function overlayAutoVersionId($table, $id)
8688
    {
8689
        $autoVersionId = $this->getAutoVersionId($table, $id);
8690
        if ($autoVersionId !== null) {
8691
            $id = $autoVersionId;
8692
        }
8693
        return $id;
8694
    }
8695
8696
    /**
8697
     * Adds new values to the remapStackChildIds array.
8698
     *
8699
     * @param array $idValues uid values
8700
     */
8701
    protected function addNewValuesToRemapStackChildIds(array $idValues)
8702
    {
8703
        foreach ($idValues as $idValue) {
8704
            if (strpos($idValue, 'NEW') === 0) {
8705
                $this->remapStackChildIds[$idValue] = true;
8706
            }
8707
        }
8708
    }
8709
8710
    /**
8711
     * Resolves versioned records for the current workspace scope.
8712
     * Delete placeholders are substituted and removed.
8713
     *
8714
     * @param string $tableName Name of the table to be processed
8715
     * @param string $fieldNames List of the field names to be fetched
8716
     * @param string $sortingField Name of the sorting field to be used
8717
     * @param array $liveIds Flat array of (live) record ids
8718
     * @return array
8719
     */
8720
    protected function resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
8721
    {
8722
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
8723
            ->getConnectionForTable($tableName);
8724
        $sortingStatement = !empty($sortingField)
8725
            ? [$connection->quoteIdentifier($sortingField)]
8726
            : null;
8727
        /** @var PlainDataResolver $resolver */
8728
        $resolver = GeneralUtility::makeInstance(
8729
            PlainDataResolver::class,
8730
            $tableName,
8731
            $liveIds,
8732
            $sortingStatement
8733
        );
8734
8735
        $resolver->setWorkspaceId($this->BE_USER->workspace);
8736
        $resolver->setKeepDeletePlaceholder(false);
8737
        $resolver->setKeepMovePlaceholder(false);
8738
        $resolver->setKeepLiveIds(true);
8739
        $recordIds = $resolver->get();
8740
8741
        $records = [];
8742
        foreach ($recordIds as $recordId) {
8743
            $records[$recordId] = BackendUtility::getRecord($tableName, $recordId, $fieldNames);
8744
        }
8745
8746
        return $records;
8747
    }
8748
8749
    /**
8750
     * Gets the outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler
8751
     * Since \TYPO3\CMS\Core\DataHandling\DataHandler can create nested objects of itself,
8752
     * this method helps to determine the first (= outer most) one.
8753
     *
8754
     * @return DataHandler
8755
     */
8756
    protected function getOuterMostInstance()
8757
    {
8758
        if (!isset($this->outerMostInstance)) {
8759
            $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
8760
            foreach ($stack as $stackItem) {
8761
                if (isset($stackItem['object']) && $stackItem['object'] instanceof self) {
8762
                    $this->outerMostInstance = $stackItem['object'];
8763
                    break;
8764
                }
8765
            }
8766
        }
8767
        return $this->outerMostInstance;
8768
    }
8769
8770
    /**
8771
     * Determines whether the this object is the outer most instance of itself
8772
     * Since DataHandler can create nested objects of itself,
8773
     * this method helps to determine the first (= outer most) one.
8774
     *
8775
     * @return bool
8776
     */
8777
    public function isOuterMostInstance()
8778
    {
8779
        return $this->getOuterMostInstance() === $this;
8780
    }
8781
8782
    /**
8783
     * Gets an instance of the runtime cache.
8784
     *
8785
     * @return FrontendInterface
8786
     */
8787
    protected function getRuntimeCache()
8788
    {
8789
        return $this->getCacheManager()->getCache('runtime');
8790
    }
8791
8792
    /**
8793
     * Determines nested element calls.
8794
     *
8795
     * @param string $table Name of the table
8796
     * @param int $id Uid of the record
8797
     * @param string $identifier Name of the action to be checked
8798
     * @return bool
8799
     */
8800
    protected function isNestedElementCallRegistered($table, $id, $identifier)
8801
    {
8802
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8803
        return isset($nestedElementCalls[$identifier][$table][$id]);
8804
    }
8805
8806
    /**
8807
     * Registers nested elements calls.
8808
     * This is used to track nested calls (e.g. for following m:n relations).
8809
     *
8810
     * @param string $table Name of the table
8811
     * @param int $id Uid of the record
8812
     * @param string $identifier Name of the action to be tracked
8813
     */
8814
    protected function registerNestedElementCall($table, $id, $identifier)
8815
    {
8816
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8817
        $nestedElementCalls[$identifier][$table][$id] = true;
8818
        $this->runtimeCache->set($this->cachePrefixNestedElementCalls, $nestedElementCalls);
8819
    }
8820
8821
    /**
8822
     * Resets the nested element calls.
8823
     */
8824
    protected function resetNestedElementCalls()
8825
    {
8826
        $this->runtimeCache->remove($this->cachePrefixNestedElementCalls);
8827
    }
8828
8829
    /**
8830
     * Determines whether an element was registered to be deleted in the registry.
8831
     *
8832
     * @param string $table Name of the table
8833
     * @param int $id Uid of the record
8834
     * @return bool
8835
     * @see registerElementsToBeDeleted
8836
     * @see resetElementsToBeDeleted
8837
     * @see copyRecord_raw
8838
     * @see versionizeRecord
8839
     */
8840
    protected function isElementToBeDeleted($table, $id)
8841
    {
8842
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8843
        return isset($elementsToBeDeleted[$table][$id]);
8844
    }
8845
8846
    /**
8847
     * Registers elements to be deleted in the registry.
8848
     *
8849
     * @see process_datamap
8850
     */
8851
    protected function registerElementsToBeDeleted()
8852
    {
8853
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8854
        $this->runtimeCache->set('core-datahandler-elementsToBeDeleted', array_merge($elementsToBeDeleted, $this->getCommandMapElements('delete')));
8855
    }
8856
8857
    /**
8858
     * Resets the elements to be deleted in the registry.
8859
     *
8860
     * @see process_datamap
8861
     */
8862
    protected function resetElementsToBeDeleted()
8863
    {
8864
        $this->runtimeCache->remove('core-datahandler-elementsToBeDeleted');
8865
    }
8866
8867
    /**
8868
     * Unsets elements (e.g. of the data map) that shall be deleted.
8869
     * This avoids to modify records that will be deleted later on.
8870
     *
8871
     * @param array $elements Elements to be modified
8872
     * @return array
8873
     */
8874
    protected function unsetElementsToBeDeleted(array $elements)
8875
    {
8876
        $elements = ArrayUtility::arrayDiffAssocRecursive($elements, $this->getCommandMapElements('delete'));
8877
        foreach ($elements as $key => $value) {
8878
            if (empty($value)) {
8879
                unset($elements[$key]);
8880
            }
8881
        }
8882
        return $elements;
8883
    }
8884
8885
    /**
8886
     * Gets elements of the command map that match a particular command.
8887
     *
8888
     * @param string $needle The command to be matched
8889
     * @return array
8890
     */
8891
    protected function getCommandMapElements($needle)
8892
    {
8893
        $elements = [];
8894
        foreach ($this->cmdmap as $tableName => $idArray) {
8895
            foreach ($idArray as $id => $commandArray) {
8896
                foreach ($commandArray as $command => $value) {
8897
                    if ($value && $command == $needle) {
8898
                        $elements[$tableName][$id] = true;
8899
                    }
8900
                }
8901
            }
8902
        }
8903
        return $elements;
8904
    }
8905
8906
    /**
8907
     * Controls active elements and sets NULL values if not active.
8908
     * Datamap is modified accordant to submitted control values.
8909
     */
8910
    protected function controlActiveElements()
8911
    {
8912
        if (!empty($this->control['active'])) {
8913
            $this->setNullValues(
8914
                $this->control['active'],
8915
                $this->datamap
8916
            );
8917
        }
8918
    }
8919
8920
    /**
8921
     * Sets NULL values in haystack array.
8922
     * The general behaviour in the user interface is to enable/activate fields.
8923
     * Thus, this method uses NULL as value to be stored if a field is not active.
8924
     *
8925
     * @param array $active hierarchical array with active elements
8926
     * @param array $haystack hierarchical array with haystack to be modified
8927
     */
8928
    protected function setNullValues(array $active, array &$haystack)
8929
    {
8930
        foreach ($active as $key => $value) {
8931
            // Nested data is processes recursively
8932
            if (is_array($value)) {
8933
                $this->setNullValues(
8934
                    $value,
8935
                    $haystack[$key]
8936
                );
8937
            } elseif ($value == 0) {
8938
                // Field has not been activated in the user interface,
8939
                // thus a NULL value shall be stored in the database
8940
                $haystack[$key] = null;
8941
            }
8942
        }
8943
    }
8944
8945
    /**
8946
     * @param CorrelationId $correlationId
8947
     */
8948
    public function setCorrelationId(CorrelationId $correlationId): void
8949
    {
8950
        $this->correlationId = $correlationId;
8951
    }
8952
8953
    /**
8954
     * @return CorrelationId|null
8955
     */
8956
    public function getCorrelationId(): ?CorrelationId
8957
    {
8958
        return $this->correlationId;
8959
    }
8960
8961
    /**
8962
     * Entry point to post process a database insert. Currently bails early unless a UID has been forced
8963
     * and the database platform is not MySQL.
8964
     *
8965
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8966
     * @param string $tableName
8967
     * @param int $suggestedUid
8968
     * @return int
8969
     */
8970
    protected function postProcessDatabaseInsert(Connection $connection, string $tableName, int $suggestedUid): int
8971
    {
8972
        if ($suggestedUid !== 0 && $connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
8973
            $this->postProcessPostgresqlInsert($connection, $tableName);
8974
            // The last inserted id on postgresql is actually the last value generated by the sequence.
8975
            // On a forced UID insert this might not be the actual value or the sequence might not even
8976
            // have generated a value yet.
8977
            // Return the actual ID we forced on insert as a surrogate.
8978
            return $suggestedUid;
8979
        }
8980
        if ($connection->getDatabasePlatform() instanceof SQLServerPlatform) {
8981
            return $this->postProcessSqlServerInsert($connection, $tableName);
8982
        }
8983
        $id = $connection->lastInsertId($tableName);
8984
        return (int)$id;
8985
    }
8986
8987
    /**
8988
     * Get the last insert ID from sql server
8989
     *
8990
     * - first checks whether doctrine might be able to fetch the ID from the
8991
     * sequence table
8992
     * - if that does not succeed it manually selects the current IDENTITY value
8993
     * from a table
8994
     * - returns 0 if both fail
8995
     *
8996
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8997
     * @param string $tableName
8998
     * @return int
8999
     * @throws \Doctrine\DBAL\DBALException
9000
     */
9001
    protected function postProcessSqlServerInsert(Connection $connection, string $tableName): int
9002
    {
9003
        $id = $connection->lastInsertId($tableName);
9004
        if (!((int)$id > 0)) {
9005
            $table = $connection->quoteIdentifier($tableName);
9006
            $result = $connection->executeQuery('SELECT IDENT_CURRENT(\'' . $table . '\') AS id')->fetch();
9007
            if (isset($result['id']) && $result['id'] > 0) {
9008
                $id = $result['id'];
9009
            }
9010
        }
9011
        return (int)$id;
9012
    }
9013
9014
    /**
9015
     * PostgreSQL works with sequences for auto increment columns. A sequence is not updated when a value is
9016
     * written to such a column. To avoid clashes when the sequence returns an existing ID this helper will
9017
     * update the sequence to the current max value of the column.
9018
     *
9019
     * @param \TYPO3\CMS\Core\Database\Connection $connection
9020
     * @param string $tableName
9021
     */
9022
    protected function postProcessPostgresqlInsert(Connection $connection, string $tableName)
9023
    {
9024
        $queryBuilder = $connection->createQueryBuilder();
9025
        $queryBuilder->getRestrictions()->removeAll();
9026
        $row = $queryBuilder->select('PGT.schemaname', 'S.relname', 'C.attname', 'T.relname AS tablename')
9027
            ->from('pg_class', 'S')
9028
            ->from('pg_depend', 'D')
9029
            ->from('pg_class', 'T')
9030
            ->from('pg_attribute', 'C')
9031
            ->from('pg_tables', 'PGT')
9032
            ->where(
9033
                $queryBuilder->expr()->eq('S.relkind', $queryBuilder->quote('S')),
9034
                $queryBuilder->expr()->eq('S.oid', $queryBuilder->quoteIdentifier('D.objid')),
9035
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('T.oid')),
9036
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('C.attrelid')),
9037
                $queryBuilder->expr()->eq('D.refobjsubid', $queryBuilder->quoteIdentifier('C.attnum')),
9038
                $queryBuilder->expr()->eq('T.relname', $queryBuilder->quoteIdentifier('PGT.tablename')),
9039
                $queryBuilder->expr()->eq('PGT.tablename', $queryBuilder->quote($tableName))
9040
            )
9041
            ->setMaxResults(1)
9042
            ->execute()
9043
            ->fetch();
9044
9045
        if ($row !== false) {
9046
            $connection->exec(
9047
                sprintf(
9048
                    'SELECT SETVAL(%s, COALESCE(MAX(%s), 0)+1, FALSE) FROM %s',
9049
                    $connection->quote($row['schemaname'] . '.' . $row['relname']),
9050
                    $connection->quoteIdentifier($row['attname']),
9051
                    $connection->quoteIdentifier($row['schemaname'] . '.' . $row['tablename'])
9052
                )
9053
            );
9054
        }
9055
    }
9056
9057
    /**
9058
     * Return the cache entry identifier for field evals
9059
     *
9060
     * @param string $additionalIdentifier
9061
     * @return string
9062
     */
9063
    protected function getFieldEvalCacheIdentifier($additionalIdentifier)
9064
    {
9065
        return 'core-datahandler-eval-' . md5($additionalIdentifier);
9066
    }
9067
9068
    /**
9069
     * @return RelationHandler
9070
     */
9071
    protected function createRelationHandlerInstance()
9072
    {
9073
        $isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces');
9074
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
9075
        $relationHandler->setWorkspaceId($this->BE_USER->workspace);
9076
        $relationHandler->setUseLiveReferenceIds($isWorkspacesLoaded);
9077
        $relationHandler->setUseLiveParentIds($isWorkspacesLoaded);
9078
        $relationHandler->setReferenceIndexUpdater($this->referenceIndexUpdater);
9079
        return $relationHandler;
9080
    }
9081
9082
    /**
9083
     * Create and returns an instance of the CacheManager
9084
     *
9085
     * @return CacheManager
9086
     */
9087
    protected function getCacheManager()
9088
    {
9089
        return GeneralUtility::makeInstance(CacheManager::class);
9090
    }
9091
9092
    /**
9093
     * Gets the resourceFactory
9094
     *
9095
     * @return ResourceFactory
9096
     */
9097
    protected function getResourceFactory()
9098
    {
9099
        return GeneralUtility::makeInstance(ResourceFactory::class);
9100
    }
9101
9102
    /**
9103
     * @return LanguageService
9104
     */
9105
    protected function getLanguageService()
9106
    {
9107
        return $GLOBALS['LANG'];
9108
    }
9109
9110
    /**
9111
     * @internal should only be used from within TYPO3 Core
9112
     * @return array
9113
     */
9114
    public function getHistoryRecords(): array
9115
    {
9116
        return $this->historyRecords;
9117
    }
9118
}
9119