Completed
Push — master ( d193fe...01e2c1 )
by
unknown
13:23
created

DataHandler::discardSubPagesAndRecordsOnPage()   B

Complexity

Conditions 10
Paths 12

Size

Total Lines 52
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 32
nc 12
nop 1
dl 0
loc 52
rs 7.6666
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\BackendWorkspaceRestriction;
42
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
43
use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface;
44
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
45
use TYPO3\CMS\Core\Database\ReferenceIndex;
46
use TYPO3\CMS\Core\Database\RelationHandler;
47
use TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore;
48
use TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor;
49
use TYPO3\CMS\Core\DataHandling\Model\CorrelationId;
50
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
51
use TYPO3\CMS\Core\Html\RteHtmlParser;
52
use TYPO3\CMS\Core\Localization\LanguageService;
53
use TYPO3\CMS\Core\Messaging\FlashMessage;
54
use TYPO3\CMS\Core\Messaging\FlashMessageService;
55
use TYPO3\CMS\Core\Resource\ResourceFactory;
56
use TYPO3\CMS\Core\Service\OpcodeCacheService;
57
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
58
use TYPO3\CMS\Core\SysLog\Action\Cache as SystemLogCacheAction;
59
use TYPO3\CMS\Core\SysLog\Action\Database as SystemLogDatabaseAction;
60
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
61
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
62
use TYPO3\CMS\Core\Type\Bitmask\Permission;
63
use TYPO3\CMS\Core\Utility\ArrayUtility;
64
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
65
use TYPO3\CMS\Core\Utility\GeneralUtility;
66
use TYPO3\CMS\Core\Utility\HttpUtility;
67
use TYPO3\CMS\Core\Utility\MathUtility;
68
use TYPO3\CMS\Core\Utility\StringUtility;
69
use TYPO3\CMS\Core\Versioning\VersionState;
70
71
/**
72
 * The main data handler class which takes care of correctly updating and inserting records.
73
 * This class was formerly known as TCEmain.
74
 *
75
 * This is the TYPO3 Core Engine class for manipulation of the database
76
 * This class is used by eg. the tce_db BE route (SimpleDataHandlerController) which provides an interface for POST forms to this class.
77
 *
78
 * Dependencies:
79
 * - $GLOBALS['TCA'] must exist
80
 * - $GLOBALS['LANG'] must exist
81
 *
82
 * Also see document 'TYPO3 Core API' for details.
83
 */
84
class DataHandler implements LoggerAwareInterface
85
{
86
    use LoggerAwareTrait;
87
88
    // *********************
89
    // Public variables you can configure before using the class:
90
    // *********************
91
    /**
92
     * If TRUE, the default log-messages will be stored. This should not be necessary if the locallang-file for the
93
     * log-display is properly configured. So disabling this will just save some database-space as the default messages are not saved.
94
     *
95
     * @var bool
96
     */
97
    public $storeLogMessages = true;
98
99
    /**
100
     * If TRUE, actions are logged to sys_log.
101
     *
102
     * @var bool
103
     */
104
    public $enableLogging = true;
105
106
    /**
107
     * If TRUE, the datamap array is reversed in the order, which is a nice thing if you're creating a whole new
108
     * bunch of records.
109
     *
110
     * @var bool
111
     */
112
    public $reverseOrder = false;
113
114
    /**
115
     * If TRUE, only fields which are different from the database values are saved! In fact, if a whole input array
116
     * is similar, it's not saved then.
117
     *
118
     * @var bool
119
     * @internal should only be used from within TYPO3 Core
120
     */
121
    public $checkSimilar = true;
122
123
    /**
124
     * This will read the record after having updated or inserted it. If anything is not properly submitted an error
125
     * is written to the log. This feature consumes extra time by selecting records
126
     *
127
     * @var bool
128
     */
129
    public $checkStoredRecords = true;
130
131
    /**
132
     * If set, values '' and 0 will equal each other when the stored records are checked.
133
     *
134
     * @var bool
135
     */
136
    public $checkStoredRecords_loose = true;
137
138
    /**
139
     * If this is set, then a page is deleted by deleting the whole branch under it (user must have
140
     * delete permissions to it all). If not set, then the page is deleted ONLY if it has no branch.
141
     *
142
     * @var bool
143
     */
144
    public $deleteTree = false;
145
146
    /**
147
     * If set, then the 'hideAtCopy' flag for tables will be ignored.
148
     *
149
     * @var bool
150
     */
151
    public $neverHideAtCopy = false;
152
153
    /**
154
     * If set, then the TCE class has been instantiated during an import action of a T3D
155
     *
156
     * @var bool
157
     */
158
    public $isImporting = false;
159
160
    /**
161
     * If set, then transformations are NOT performed on the input.
162
     *
163
     * @var bool
164
     */
165
    public $dontProcessTransformations = false;
166
167
    /**
168
     * Will distinguish between translations (with parent) and localizations (without parent) while still using the same methods to copy the records
169
     * TRUE: translation of a record connected to the default language
170
     * FALSE: localization of a record without connection to the default language
171
     *
172
     * @var bool
173
     */
174
    protected $useTransOrigPointerField = true;
175
176
    /**
177
     * If TRUE, workspace restrictions are bypassed on edit and create actions (process_datamap()).
178
     * YOU MUST KNOW what you do if you use this feature!
179
     *
180
     * @var bool
181
     * @internal should only be used from within TYPO3 Core
182
     */
183
    public $bypassWorkspaceRestrictions = false;
184
185
    /**
186
     * If TRUE, access check, check for deleted etc. for records is bypassed.
187
     * YOU MUST KNOW what you are doing if you use this feature!
188
     *
189
     * @var bool
190
     */
191
    public $bypassAccessCheckForRecords = false;
192
193
    /**
194
     * Comma-separated list. This list of tables decides which tables will be copied. If empty then none will.
195
     * If '*' then all will (that the user has permission to of course)
196
     *
197
     * @var string
198
     * @internal should only be used from within TYPO3 Core
199
     */
200
    public $copyWhichTables = '*';
201
202
    /**
203
     * If 0 then branch is NOT copied.
204
     * If 1 then pages on the 1st level is copied.
205
     * If 2 then pages on the second level is copied ... and so on
206
     *
207
     * @var int
208
     */
209
    public $copyTree = 0;
210
211
    /**
212
     * [table][fields]=value: New records are created with default values and you can set this array on the
213
     * form $defaultValues[$table][$field] = $value to override the default values fetched from TCA.
214
     * If ->setDefaultsFromUserTS is called UserTSconfig default values will overrule existing values in this array
215
     * (thus UserTSconfig overrules externally set defaults which overrules TCA defaults)
216
     *
217
     * @var array
218
     * @internal should only be used from within TYPO3 Core
219
     */
220
    public $defaultValues = [];
221
222
    /**
223
     * [table][fields]=value: You can set this array on the form $overrideValues[$table][$field] = $value to
224
     * override the incoming data. You must set this externally. You must make sure the fields in this array are also
225
     * found in the table, because it's not checked. All columns can be set by this array!
226
     *
227
     * @var array
228
     * @internal should only be used from within TYPO3 Core
229
     */
230
    public $overrideValues = [];
231
232
    /**
233
     * If entries are set in this array corresponding to fields for update, they are ignored and thus NOT updated.
234
     * You could set this array from a series of checkboxes with value=0 and hidden fields before the checkbox with 1.
235
     * Then an empty checkbox will disable the field.
236
     *
237
     * @var array
238
     * @internal should only be used from within TYPO3 Core
239
     */
240
    public $data_disableFields = [];
241
242
    /**
243
     * Use this array to validate suggested uids for tables by setting [table]:[uid]. This is a dangerous option
244
     * since it will force the inserted record to have a certain UID. The value just have to be TRUE, but if you set
245
     * it to "DELETE" it will make sure any record with that UID will be deleted first (raw delete).
246
     * The option is used for import of T3D files when synchronizing between two mirrored servers.
247
     * As a security measure this feature is available only for Admin Users (for now)
248
     *
249
     * @var array
250
     */
251
    public $suggestedInsertUids = [];
252
253
    /**
254
     * Object. Call back object for FlexForm traversal. Useful when external classes wants to use the
255
     * iteration functions inside DataHandler for traversing a FlexForm structure.
256
     *
257
     * @var object
258
     * @internal should only be used from within TYPO3 Core
259
     */
260
    public $callBackObj;
261
262
    /**
263
     * A string which can be used as correlationId for RecordHistory entries.
264
     * The string can later be used to rollback multiple changes at once.
265
     *
266
     * @var CorrelationId|null
267
     */
268
    protected $correlationId;
269
270
    // *********************
271
    // Internal variables (mapping arrays) which can be used (read-only) from outside
272
    // *********************
273
    /**
274
     * Contains mapping of auto-versionized records.
275
     *
276
     * @var array
277
     * @internal should only be used from within TYPO3 Core
278
     */
279
    public $autoVersionIdMap = [];
280
281
    /**
282
     * 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
283
     *
284
     * @var array
285
     */
286
    public $substNEWwithIDs = [];
287
288
    /**
289
     * Like $substNEWwithIDs, but where each old "NEW..." id is mapped to the table it was from.
290
     *
291
     * @var array
292
     * @internal should only be used from within TYPO3 Core
293
     */
294
    public $substNEWwithIDs_table = [];
295
296
    /**
297
     * Holds the tables and there the ids of newly created child records from IRRE
298
     *
299
     * @var array
300
     * @internal should only be used from within TYPO3 Core
301
     */
302
    public $newRelatedIDs = [];
303
304
    /**
305
     * This array is the sum of all copying operations in this class.
306
     *
307
     * @var array
308
     * @internal should only be used from within TYPO3 Core
309
     */
310
    public $copyMappingArray_merged = [];
311
312
    /**
313
     * Per-table array with UIDs that have been deleted.
314
     *
315
     * @var array
316
     */
317
    protected $deletedRecords = [];
318
319
    /**
320
     * Errors are collected in this variable.
321
     *
322
     * @var array
323
     * @internal should only be used from within TYPO3 Core
324
     */
325
    public $errorLog = [];
326
327
    /**
328
     * Fields from the pages-table for which changes will trigger a pagetree refresh
329
     *
330
     * @var array
331
     */
332
    public $pagetreeRefreshFieldsFromPages = ['pid', 'sorting', 'deleted', 'hidden', 'title', 'doktype', 'is_siteroot', 'fe_group', 'nav_hide', 'nav_title', 'module', 'starttime', 'endtime', 'content_from_pid', 'extendToSubpages'];
333
334
    /**
335
     * Indicates whether the pagetree needs a refresh because of important changes
336
     *
337
     * @var bool
338
     * @internal should only be used from within TYPO3 Core
339
     */
340
    public $pagetreeNeedsRefresh = false;
341
342
    // *********************
343
    // Internal Variables, do not touch.
344
    // *********************
345
346
    // Variables set in init() function:
347
348
    /**
349
     * The user-object the script uses. If not set from outside, this is set to the current global $BE_USER.
350
     *
351
     * @var BackendUserAuthentication
352
     */
353
    public $BE_USER;
354
355
    /**
356
     * Will be set to uid of be_user executing this script
357
     *
358
     * @var int
359
     * @internal should only be used from within TYPO3 Core
360
     */
361
    public $userid;
362
363
    /**
364
     * Will be set to username of be_user executing this script
365
     *
366
     * @var string
367
     * @internal should only be used from within TYPO3 Core
368
     */
369
    public $username;
370
371
    /**
372
     * Will be set if user is admin
373
     *
374
     * @var bool
375
     * @internal should only be used from within TYPO3 Core
376
     */
377
    public $admin;
378
379
    /**
380
     * @var PagePermissionAssembler
381
     */
382
    protected $pagePermissionAssembler;
383
384
    /**
385
     * 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.
386
     *
387
     * @var array
388
     */
389
    protected $excludedTablesAndFields = [];
390
391
    /**
392
     * Data submitted from the form view, used to control behaviours,
393
     * e.g. this is used to activate/deactivate fields and thus store NULL values
394
     *
395
     * @var array
396
     */
397
    protected $control = [];
398
399
    /**
400
     * Set with incoming data array
401
     *
402
     * @var array
403
     */
404
    public $datamap = [];
405
406
    /**
407
     * Set with incoming cmd array
408
     *
409
     * @var array
410
     */
411
    public $cmdmap = [];
412
413
    /**
414
     * List of changed old record ids to new records ids
415
     *
416
     * @var array
417
     */
418
    protected $mmHistoryRecords = [];
419
420
    /**
421
     * List of changed old record ids to new records ids
422
     *
423
     * @var array
424
     */
425
    protected $historyRecords = [];
426
427
    // Internal static:
428
429
    /**
430
     * The interval between sorting numbers used with tables with a 'sorting' field defined.
431
     *
432
     * Min 1, should be power of 2
433
     *
434
     * @var int
435
     * @internal should only be used from within TYPO3 Core
436
     */
437
    public $sortIntervals = 256;
438
439
    // Internal caching arrays
440
    /**
441
     * User by function checkRecordInsertAccess() to store whether a record can be inserted on a page id
442
     *
443
     * @var array
444
     */
445
    protected $recInsertAccessCache = [];
446
447
    /**
448
     * Caching array for check of whether records are in a webmount
449
     *
450
     * @var array
451
     */
452
    protected $isRecordInWebMount_Cache = [];
453
454
    /**
455
     * Caching array for page ids in webmounts
456
     *
457
     * @var array
458
     */
459
    protected $isInWebMount_Cache = [];
460
461
    /**
462
     * Used for caching page records in pageInfo()
463
     *
464
     * @var array
465
     */
466
    protected $pageCache = [];
467
468
    // Other arrays:
469
    /**
470
     * For accumulation of MM relations that must be written after new records are created.
471
     *
472
     * @var array
473
     * @internal
474
     */
475
    public $dbAnalysisStore = [];
476
477
    /**
478
     * Used for tracking references that might need correction after operations
479
     *
480
     * @var array
481
     * @internal
482
     */
483
    public $registerDBList = [];
484
485
    /**
486
     * Used for tracking references that might need correction in pid field after operations (e.g. IRRE)
487
     *
488
     * @var array
489
     * @internal
490
     */
491
    public $registerDBPids = [];
492
493
    /**
494
     * Used by the copy action to track the ids of new pages so subpages are correctly inserted!
495
     * THIS is internally cleared for each executed copy operation! DO NOT USE THIS FROM OUTSIDE!
496
     * Read from copyMappingArray_merged instead which is accumulating this information.
497
     *
498
     * NOTE: This is used by some outside scripts (e.g. hooks), as the results in $copyMappingArray_merged
499
     * are only available after an action has been completed.
500
     *
501
     * @var array
502
     * @internal
503
     */
504
    public $copyMappingArray = [];
505
506
    /**
507
     * Array used for remapping uids and values at the end of process_datamap
508
     *
509
     * @var array
510
     * @internal
511
     */
512
    public $remapStack = [];
513
514
    /**
515
     * Array used for remapping uids and values at the end of process_datamap
516
     * (e.g. $remapStackRecords[<table>][<uid>] = <index in $remapStack>)
517
     *
518
     * @var array
519
     * @internal
520
     */
521
    public $remapStackRecords = [];
522
523
    /**
524
     * Array used for checking whether new children need to be remapped
525
     *
526
     * @var array
527
     */
528
    protected $remapStackChildIds = [];
529
530
    /**
531
     * Array used for executing addition actions after remapping happened (set processRemapStack())
532
     *
533
     * @var array
534
     */
535
    protected $remapStackActions = [];
536
537
    /**
538
     * Array used for executing post-processing on the reference index
539
     *
540
     * @var array
541
     */
542
    protected $remapStackRefIndex = [];
543
544
    /**
545
     * Array used for additional calls to $this->updateRefIndex
546
     *
547
     * @var array
548
     * @internal
549
     */
550
    public $updateRefIndexStack = [];
551
552
    /**
553
     * Tells, that this DataHandler instance was called from \TYPO3\CMS\Impext\ImportExport.
554
     * This variable is set by \TYPO3\CMS\Impext\ImportExport
555
     *
556
     * @var bool
557
     * @internal only used within TYPO3 Core
558
     */
559
    public $callFromImpExp = false;
560
561
    // Various
562
563
    /**
564
     * Set to "currentRecord" during checking of values.
565
     *
566
     * @var array
567
     * @internal
568
     */
569
    public $checkValue_currentRecord = [];
570
571
    /**
572
     * Disable delete clause
573
     *
574
     * @var bool
575
     */
576
    protected $disableDeleteClause = false;
577
578
    /**
579
     * @var array
580
     */
581
    protected $checkModifyAccessListHookObjects;
582
583
    /**
584
     * @var array
585
     */
586
    protected $version_remapMMForVersionSwap_reg;
587
588
    /**
589
     * The outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler:
590
     * This object instantiates itself on versioning and localization ...
591
     *
592
     * @var \TYPO3\CMS\Core\DataHandling\DataHandler
593
     */
594
    protected $outerMostInstance;
595
596
    /**
597
     * Internal cache for collecting records that should trigger cache clearing
598
     *
599
     * @var array
600
     */
601
    protected static $recordsToClearCacheFor = [];
602
603
    /**
604
     * Internal cache for pids of records which were deleted. It's not possible
605
     * to retrieve the parent folder/page at a later stage
606
     *
607
     * @var array
608
     */
609
    protected static $recordPidsForDeletedRecords = [];
610
611
    /**
612
     * Runtime Cache to store and retrieve data computed for a single request
613
     *
614
     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
615
     */
616
    protected $runtimeCache;
617
618
    /**
619
     * Prefix for the cache entries of nested element calls since the runtimeCache has a global scope.
620
     *
621
     * @var string
622
     */
623
    protected $cachePrefixNestedElementCalls = 'core-datahandler-nestedElementCalls-';
624
625
    /**
626
     * Sets up the data handler cache and some additional options, the main logic is done in the start() method.
627
     */
628
    public function __construct()
629
    {
630
        $this->checkStoredRecords = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecords'];
631
        $this->checkStoredRecords_loose = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecordsLoose'];
632
        $this->runtimeCache = $this->getRuntimeCache();
633
        $this->pagePermissionAssembler = GeneralUtility::makeInstance(PagePermissionAssembler::class, $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions']);
634
    }
635
636
    /**
637
     * @param array $control
638
     * @internal
639
     */
640
    public function setControl(array $control)
641
    {
642
        $this->control = $control;
643
    }
644
645
    /**
646
     * Initializing.
647
     * For details, see 'TYPO3 Core API' document.
648
     * This function does not start the processing of data, but merely initializes the object
649
     *
650
     * @param array $data Data to be modified or inserted in the database
651
     * @param array $cmd Commands to copy, move, delete, localize, versionize records.
652
     * @param BackendUserAuthentication|null $altUserObject An alternative userobject you can set instead of the default, which is $GLOBALS['BE_USER']
653
     */
654
    public function start($data, $cmd, $altUserObject = null)
655
    {
656
        // Initializing BE_USER
657
        $this->BE_USER = is_object($altUserObject) ? $altUserObject : $GLOBALS['BE_USER'];
658
        $this->userid = $this->BE_USER->user['uid'] ?? 0;
659
        $this->username = $this->BE_USER->user['username'] ?? '';
660
        $this->admin = $this->BE_USER->user['admin'] ?? false;
661
        if ($this->BE_USER->uc['recursiveDelete'] ?? false) {
662
            $this->deleteTree = 1;
0 ignored issues
show
Documentation Bug introduced by
The property $deleteTree was declared of type boolean, but 1 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
663
        }
664
665
        // set correlation id for each new set of data or commands
666
        $this->correlationId = CorrelationId::forScope(
667
            md5(StringUtility::getUniqueId(self::class))
668
        );
669
670
        // Get default values from user TSconfig
671
        $tcaDefaultOverride = $this->BE_USER->getTSConfig()['TCAdefaults.'] ?? null;
672
        if (is_array($tcaDefaultOverride)) {
673
            $this->setDefaultsFromUserTS($tcaDefaultOverride);
674
        }
675
676
        // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
677
        if (!$this->admin) {
678
            $this->excludedTablesAndFields = array_flip($this->getExcludeListArray());
679
        }
680
        // Setting the data and cmd arrays
681
        if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
682
            reset($data);
683
            $this->datamap = $data;
684
        }
685
        if (is_array($cmd)) {
0 ignored issues
show
introduced by
The condition is_array($cmd) is always true.
Loading history...
686
            reset($cmd);
687
            $this->cmdmap = $cmd;
688
        }
689
    }
690
691
    /**
692
     * Function that can mirror input values in datamap-array to other uid numbers.
693
     * 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]
694
     *
695
     * @param array $mirror This array has the syntax $mirror[table_name][uid] = [list of uids to copy data-value TO!]
696
     * @internal
697
     */
698
    public function setMirror($mirror)
699
    {
700
        if (!is_array($mirror)) {
0 ignored issues
show
introduced by
The condition is_array($mirror) is always true.
Loading history...
701
            return;
702
        }
703
704
        foreach ($mirror as $table => $uid_array) {
705
            if (!isset($this->datamap[$table])) {
706
                continue;
707
            }
708
709
            foreach ($uid_array as $id => $uidList) {
710
                if (!isset($this->datamap[$table][$id])) {
711
                    continue;
712
                }
713
714
                $theIdsInArray = GeneralUtility::trimExplode(',', $uidList, true);
715
                foreach ($theIdsInArray as $copyToUid) {
716
                    $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
717
                }
718
            }
719
        }
720
    }
721
722
    /**
723
     * Initializes default values coming from User TSconfig
724
     *
725
     * @param array $userTS User TSconfig array
726
     * @internal should only be used from within DataHandler
727
     */
728
    public function setDefaultsFromUserTS($userTS)
729
    {
730
        if (!is_array($userTS)) {
0 ignored issues
show
introduced by
The condition is_array($userTS) is always true.
Loading history...
731
            return;
732
        }
733
734
        foreach ($userTS as $k => $v) {
735
            $k = mb_substr($k, 0, -1);
736
            if (!$k || !is_array($v) || !isset($GLOBALS['TCA'][$k])) {
737
                continue;
738
            }
739
740
            if (is_array($this->defaultValues[$k])) {
741
                $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
742
            } else {
743
                $this->defaultValues[$k] = $v;
744
            }
745
        }
746
    }
747
748
    /**
749
     * When a new record is created, all values that haven't been set but are set via PageTSconfig / UserTSconfig
750
     * get applied here.
751
     *
752
     * This is only executed for new records. The most important part is that the pageTS of the actual resolved $pid
753
     * is taken, and a new field array with empty defaults is set again.
754
     *
755
     * @param string $table
756
     * @param int $pageId
757
     * @param array $prepopulatedFieldArray
758
     * @return array
759
     */
760
    protected function applyDefaultsForFieldArray(string $table, int $pageId, array $prepopulatedFieldArray): array
761
    {
762
        // First set TCAdefaults respecting the given PageID
763
        $tcaDefaults = BackendUtility::getPagesTSconfig($pageId)['TCAdefaults.'] ?? null;
764
        // Re-apply $this->defaultValues settings
765
        $this->setDefaultsFromUserTS($tcaDefaults);
766
        $cleanFieldArray = $this->newFieldArray($table);
767
        if (isset($prepopulatedFieldArray['pid'])) {
768
            $cleanFieldArray['pid'] = $prepopulatedFieldArray['pid'];
769
        }
770
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? null;
771
        if ($sortColumn !== null && isset($prepopulatedFieldArray[$sortColumn])) {
772
            $cleanFieldArray[$sortColumn] = $prepopulatedFieldArray[$sortColumn];
773
        }
774
        return $cleanFieldArray;
775
    }
776
777
    /*********************************************
778
     *
779
     * HOOKS
780
     *
781
     *********************************************/
782
    /**
783
     * Hook: processDatamap_afterDatabaseOperations
784
     * (calls $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);)
785
     *
786
     * Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
787
     * but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
788
     *
789
     * @param array $hookObjectsArr (reference) Array with hook objects
790
     * @param string $status (reference) Status of the current operation, 'new' or 'update
791
     * @param string $table (reference) The table currently processing data for
792
     * @param string $id (reference) The record uid currently processing data for, [integer] or [string] (like 'NEW...')
793
     * @param array $fieldArray (reference) The field array of a record
794
     * @internal should only be used from within DataHandler
795
     */
796
    public function hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
797
    {
798
        // Process hook directly:
799
        if (!isset($this->remapStackRecords[$table][$id])) {
800
            foreach ($hookObjectsArr as $hookObj) {
801
                if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
802
                    $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
803
                }
804
            }
805
        } else {
806
            $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = [
807
                'status' => $status,
808
                'fieldArray' => $fieldArray,
809
                'hookObjectsArr' => $hookObjectsArr
810
            ];
811
        }
812
    }
813
814
    /**
815
     * Gets the 'checkModifyAccessList' hook objects.
816
     * The first call initializes the accordant objects.
817
     *
818
     * @return array The 'checkModifyAccessList' hook objects (if any)
819
     * @throws \UnexpectedValueException
820
     */
821
    protected function getCheckModifyAccessListHookObjects()
822
    {
823
        if (!isset($this->checkModifyAccessListHookObjects)) {
824
            $this->checkModifyAccessListHookObjects = [];
825
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] ?? [] as $className) {
826
                $hookObject = GeneralUtility::makeInstance($className);
827
                if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
828
                    throw new \UnexpectedValueException($className . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
829
                }
830
                $this->checkModifyAccessListHookObjects[] = $hookObject;
831
            }
832
        }
833
        return $this->checkModifyAccessListHookObjects;
834
    }
835
836
    /*********************************************
837
     *
838
     * PROCESSING DATA
839
     *
840
     *********************************************/
841
    /**
842
     * Processing the data-array
843
     * Call this function to process the data-array set by start()
844
     *
845
     * @return bool|void
846
     */
847
    public function process_datamap()
848
    {
849
        $this->controlActiveElements();
850
851
        // Keep versionized(!) relations here locally:
852
        $registerDBList = [];
853
        $this->registerElementsToBeDeleted();
854
        $this->datamap = $this->unsetElementsToBeDeleted($this->datamap);
855
        // Editing frozen:
856
        if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
857
            $this->newlog('All editing in this workspace has been frozen!', SystemLogErrorClassification::USER_ERROR);
858
            return false;
859
        }
860
        // First prepare user defined objects (if any) for hooks which extend this function:
861
        $hookObjectsArr = [];
862
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] ?? [] as $className) {
863
            $hookObject = GeneralUtility::makeInstance($className);
864
            if (method_exists($hookObject, 'processDatamap_beforeStart')) {
865
                $hookObject->processDatamap_beforeStart($this);
866
            }
867
            $hookObjectsArr[] = $hookObject;
868
        }
869
        // Pre-process data-map and synchronize localization states
870
        $this->datamap = GeneralUtility::makeInstance(SlugEnricher::class)->enrichDataMap($this->datamap);
871
        $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER)->process();
872
        // 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.
873
        $orderOfTables = [];
874
        // Set pages first.
875
        if (isset($this->datamap['pages'])) {
876
            $orderOfTables[] = 'pages';
877
        }
878
        $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
879
        // Process the tables...
880
        foreach ($orderOfTables as $table) {
881
            // Check if
882
            //	   - table is set in $GLOBALS['TCA'],
883
            //	   - table is NOT readOnly
884
            //	   - the table is set with content in the data-array (if not, there's nothing to process...)
885
            //	   - permissions for tableaccess OK
886
            $modifyAccessList = $this->checkModifyAccessList($table);
887
            if (!$modifyAccessList) {
888
                $this->log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
889
            }
890
            if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
891
                continue;
892
            }
893
894
            if ($this->reverseOrder) {
895
                $this->datamap[$table] = array_reverse($this->datamap[$table], 1);
896
            }
897
            // For each record from the table, do:
898
            // $id is the record uid, may be a string if new records...
899
            // $incomingFieldArray is the array of fields
900
            foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
901
                if (!is_array($incomingFieldArray)) {
902
                    continue;
903
                }
904
                $theRealPid = null;
905
906
                // Hook: processDatamap_preProcessFieldArray
907
                foreach ($hookObjectsArr as $hookObj) {
908
                    if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
909
                        $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
910
                    }
911
                }
912
                // ******************************
913
                // Checking access to the record
914
                // ******************************
915
                $createNewVersion = false;
916
                $recordAccess = false;
917
                $old_pid_value = '';
918
                // Is it a new record? (Then Id is a string)
919
                if (!MathUtility::canBeInterpretedAsInteger($id)) {
920
                    // Get a fieldArray with tca default values
921
                    $fieldArray = $this->newFieldArray($table);
922
                    // A pid must be set for new records.
923
                    if (isset($incomingFieldArray['pid'])) {
924
                        $pid_value = $incomingFieldArray['pid'];
925
                        // Checking and finding numerical pid, it may be a string-reference to another value
926
                        $canProceed = true;
927
                        // If a NEW... id
928
                        if (strpos($pid_value, 'NEW') !== false) {
929
                            if ($pid_value[0] === '-') {
930
                                $negFlag = -1;
931
                                $pid_value = substr($pid_value, 1);
932
                            } else {
933
                                $negFlag = 1;
934
                            }
935
                            // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
936
                            if (isset($this->substNEWwithIDs[$pid_value])) {
937
                                if ($negFlag === 1) {
938
                                    $old_pid_value = $this->substNEWwithIDs[$pid_value];
939
                                }
940
                                $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
941
                            } else {
942
                                $canProceed = false;
943
                            }
944
                        }
945
                        $pid_value = (int)$pid_value;
946
                        if ($canProceed) {
947
                            $fieldArray = $this->resolveSortingAndPidForNewRecord($table, $pid_value, $fieldArray);
948
                        }
949
                    }
950
                    $theRealPid = $fieldArray['pid'];
951
                    // Checks if records can be inserted on this $pid.
952
                    // If this is a page translation, the check needs to be done for the l10n_parent record
953
                    if ($table === 'pages' && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0 && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0) {
954
                        $recordAccess = $this->checkRecordInsertAccess($table, $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]);
955
                    } else {
956
                        $recordAccess = $this->checkRecordInsertAccess($table, $theRealPid);
957
                    }
958
                    if ($recordAccess) {
959
                        $this->addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray);
960
                        $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
961
                        if (!$recordAccess) {
962
                            $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
963
                        } elseif (!$this->bypassWorkspaceRestrictions && !$this->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
964
                            // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
965
                            // So, if no live records were allowed in the current workspace, we have to create a new version of this record
966
                            if (BackendUtility::isTableWorkspaceEnabled($table)) {
967
                                $createNewVersion = true;
968
                            } else {
969
                                $recordAccess = false;
970
                                $this->newlog('Record could not be created in this workspace', SystemLogErrorClassification::USER_ERROR);
971
                            }
972
                        }
973
                    }
974
                    // Yes new record, change $record_status to 'insert'
975
                    $status = 'new';
976
                } else {
977
                    // Nope... $id is a number
978
                    $fieldArray = [];
979
                    $recordAccess = $this->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $hookObjectsArr);
980
                    if (!$recordAccess) {
981
                        if ($this->enableLogging) {
982
                            $propArr = $this->getRecordProperties($table, $id);
983
                            $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']);
984
                        }
985
                        continue;
986
                    }
987
                    // Next check of the record permissions (internals)
988
                    $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
989
                    if (!$recordAccess) {
990
                        $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
991
                    } else {
992
                        // Here we fetch the PID of the record that we point to...
993
                        $tempdata = $this->recordInfo($table, $id, 'pid' . (BackendUtility::isTableWorkspaceEnabled($table) ? ',t3ver_oid,t3ver_wsid,t3ver_stage' : ''));
994
                        $theRealPid = $tempdata['pid'] ?? null;
995
                        // Use the new id of the versionized record we're trying to write to:
996
                        // (This record is a child record of a parent and has already been versionized.)
997
                        if (!empty($this->autoVersionIdMap[$table][$id])) {
998
                            // For the reason that creating a new version of this record, automatically
999
                            // created related child records (e.g. "IRRE"), update the accordant field:
1000
                            $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1001
                            // Use the new id of the copied/versionized record:
1002
                            $id = $this->autoVersionIdMap[$table][$id];
1003
                            $recordAccess = true;
1004
                        } elseif (!$this->bypassWorkspaceRestrictions && ($errorCode = $this->BE_USER->workspaceCannotEditRecord($table, $tempdata))) {
1005
                            $recordAccess = false;
1006
                            // Versioning is required and it must be offline version!
1007
                            // Check if there already is a workspace version
1008
                            $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
1009
                            if ($workspaceVersion) {
1010
                                $id = $workspaceVersion['uid'];
1011
                                $recordAccess = true;
1012
                            } elseif ($this->BE_USER->workspaceAllowAutoCreation($table, $id, $theRealPid)) {
1013
                                // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1014
                                $this->pagetreeNeedsRefresh = true;
1015
1016
                                /** @var DataHandler $tce */
1017
                                $tce = GeneralUtility::makeInstance(__CLASS__);
1018
                                $tce->enableLogging = $this->enableLogging;
1019
                                // Setting up command for creating a new version of the record:
1020
                                $cmd = [];
1021
                                $cmd[$table][$id]['version'] = [
1022
                                    'action' => 'new',
1023
                                    // Default is to create a version of the individual records
1024
                                    'label' => 'Auto-created for WS #' . $this->BE_USER->workspace
1025
                                ];
1026
                                $tce->start([], $cmd, $this->BE_USER);
1027
                                $tce->process_cmdmap();
1028
                                $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
1029
                                // If copying was successful, share the new uids (also of related children):
1030
                                if (!empty($tce->copyMappingArray[$table][$id])) {
1031
                                    foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
1032
                                        foreach ($origIdArray as $origId => $newId) {
1033
                                            $this->autoVersionIdMap[$origTable][$origId] = $newId;
1034
                                        }
1035
                                    }
1036
                                    // Update registerDBList, that holds the copied relations to child records:
1037
                                    $registerDBList = array_merge($registerDBList, $tce->registerDBList);
1038
                                    // For the reason that creating a new version of this record, automatically
1039
                                    // created related child records (e.g. "IRRE"), update the accordant field:
1040
                                    $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1041
                                    // Use the new id of the copied/versionized record:
1042
                                    $id = $this->autoVersionIdMap[$table][$id];
1043
                                    $recordAccess = true;
1044
                                } else {
1045
                                    $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);
1046
                                }
1047
                            } else {
1048
                                $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);
1049
                            }
1050
                        }
1051
                    }
1052
                    // The default is 'update'
1053
                    $status = 'update';
1054
                }
1055
                // If access was granted above, proceed to create or update record:
1056
                if (!$recordAccess) {
1057
                    continue;
1058
                }
1059
1060
                // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
1061
                [$tscPID] = BackendUtility::getTSCpid($table, $id, $old_pid_value ?: $fieldArray['pid']);
1062
                if ($status === 'new') {
1063
                    // Apply TCAdefaults from pageTS
1064
                    $fieldArray = $this->applyDefaultsForFieldArray($table, (int)$tscPID, $fieldArray);
1065
                    // Apply page permissions as well
1066
                    if ($table === 'pages') {
1067
                        $fieldArray = $this->pagePermissionAssembler->applyDefaults(
1068
                            $fieldArray,
1069
                            (int)$tscPID,
1070
                            (int)$this->userid,
1071
                            (int)$this->BE_USER->firstMainGroup
1072
                        );
1073
                    }
1074
                }
1075
                // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1076
                $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1077
                $newVersion_placeholderFieldArray = [];
1078
                if ($createNewVersion) {
1079
                    // create a placeholder array with already processed field content
1080
                    $newVersion_placeholderFieldArray = $fieldArray;
1081
                }
1082
                // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations to field!
1083
                // Forcing some values unto field array:
1084
                // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1085
                $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1086
                if ($createNewVersion) {
1087
                    $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1088
                }
1089
                // Setting system fields
1090
                if ($status === 'new') {
1091
                    if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1092
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1093
                        if ($createNewVersion) {
1094
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1095
                        }
1096
                    }
1097
                    if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1098
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1099
                        if ($createNewVersion) {
1100
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1101
                        }
1102
                    }
1103
                } elseif ($this->checkSimilar) {
1104
                    // Removing fields which are equal to the current value:
1105
                    $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1106
                }
1107
                if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1108
                    $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1109
                    if ($createNewVersion) {
1110
                        $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1111
                    }
1112
                }
1113
                // Set stage to "Editing" to make sure we restart the workflow
1114
                if (BackendUtility::isTableWorkspaceEnabled($table)) {
1115
                    $fieldArray['t3ver_stage'] = 0;
1116
                }
1117
                // Hook: processDatamap_postProcessFieldArray
1118
                foreach ($hookObjectsArr as $hookObj) {
1119
                    if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1120
                        $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1121
                    }
1122
                }
1123
                // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1124
                // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already
1125
                if (is_array($fieldArray)) {
1126
                    if ($status === 'new') {
1127
                        if ($table === 'pages') {
1128
                            // for new pages always a refresh is needed
1129
                            $this->pagetreeNeedsRefresh = true;
1130
                        }
1131
1132
                        // This creates a new version of the record with online placeholder and offline version
1133
                        if ($createNewVersion) {
1134
                            // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1135
                            $this->pagetreeNeedsRefresh = true;
1136
1137
                            // Setting placeholder state value for temporary record
1138
                            $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1139
                            // Setting workspace - only so display of placeholders can filter out those from other workspaces.
1140
                            $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1141
                            // Only set a label if it is an input field
1142
                            $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1143
                            if ($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['type'] === 'input') {
1144
                                $newVersion_placeholderFieldArray[$labelField] = $this->getPlaceholderTitleForTableLabel($table);
1145
                            }
1146
                            // Saving placeholder as 'original'
1147
                            $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1148
                            // For the actual new offline version, set versioning values to point to placeholder
1149
                            $fieldArray['pid'] = $theRealPid;
1150
                            $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1151
                            // Setting placeholder state value for version (so it can know it is currently a new version...)
1152
                            $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1153
                            $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1154
                            // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1155
                            $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1156
                            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...
1157
                                // Processes fields of the placeholder record:
1158
                                $this->triggerRemapAction($table, $id, [$this, 'placeholderShadowing'], [$table, $phShadowId]);
1159
                                // Hold auto-versionized ids of placeholders:
1160
                                $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $phShadowId;
1161
                            }
1162
                        } else {
1163
                            $this->insertDB($table, $id, $fieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1164
                        }
1165
                    } else {
1166
                        if ($table === 'pages') {
1167
                            // only a certain number of fields needs to be checked for updates
1168
                            // if $this->checkSimilar is TRUE, fields with unchanged values are already removed here
1169
                            $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1170
                            if (!empty($fieldsToCheck)) {
1171
                                $this->pagetreeNeedsRefresh = true;
1172
                            }
1173
                        }
1174
                        $this->updateDB($table, $id, $fieldArray);
1175
                        $this->placeholderShadowing($table, $id);
1176
                    }
1177
                }
1178
                // Hook: processDatamap_afterDatabaseOperations
1179
                // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1180
                // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1181
                $this->hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1182
            }
1183
        }
1184
        // Process the stack of relations to remap/correct
1185
        $this->processRemapStack();
1186
        $this->dbAnalysisStoreExec();
1187
        // Hook: processDatamap_afterAllOperations
1188
        // Note: When this hook gets called, all operations on the submitted data have been finished.
1189
        foreach ($hookObjectsArr as $hookObj) {
1190
            if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1191
                $hookObj->processDatamap_afterAllOperations($this);
1192
            }
1193
        }
1194
        if ($this->isOuterMostInstance()) {
1195
            $this->processClearCacheQueue();
1196
            $this->resetElementsToBeDeleted();
1197
        }
1198
    }
1199
1200
    /**
1201
     * @param string $table
1202
     * @param string $value
1203
     * @param string $dbType
1204
     * @return string
1205
     */
1206
    protected function normalizeTimeFormat(string $table, string $value, string $dbType): string
1207
    {
1208
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1209
        $platform = $connection->getDatabasePlatform();
1210
        if ($platform instanceof SQLServerPlatform) {
1211
            $defaultLength = QueryHelper::getDateTimeFormats()[$dbType]['empty'];
1212
            $value = substr(
1213
                $value,
1214
                0,
1215
                strlen($defaultLength)
1216
            );
1217
        }
1218
        return $value;
1219
    }
1220
1221
    /**
1222
     * Sets the "sorting" DB field and the "pid" field of an incoming record that should be added (NEW1234)
1223
     * depending on the record that should be added or where it should be added.
1224
     *
1225
     * This method is called from process_datamap()
1226
     *
1227
     * @param string $table the table name of the record to insert
1228
     * @param int $pid the real PID (numeric) where the record should be
1229
     * @param array $fieldArray field+value pairs to add
1230
     * @return array the modified field array
1231
     */
1232
    protected function resolveSortingAndPidForNewRecord(string $table, int $pid, array $fieldArray): array
1233
    {
1234
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
1235
        // Points to a page on which to insert the element, possibly in the top of the page
1236
        if ($pid >= 0) {
1237
            // Ensure that the "pid" is not a translated page ID, but the default page ID
1238
            $pid = $this->getDefaultLanguagePageId($pid);
1239
            // The numerical pid is inserted in the data array
1240
            $fieldArray['pid'] = $pid;
1241
            // If this table is sorted we better find the top sorting number
1242
            if ($sortColumn) {
1243
                $fieldArray[$sortColumn] = $this->getSortNumber($table, 0, $pid);
1244
            }
1245
        } elseif ($sortColumn) {
1246
            // Points to another record before itself
1247
            // If this table is sorted we better find the top sorting number
1248
            // Because $pid is < 0, getSortNumber() returns an array
1249
            $sortingInfo = $this->getSortNumber($table, 0, $pid);
1250
            $fieldArray['pid'] = $sortingInfo['pid'];
1251
            $fieldArray[$sortColumn] = $sortingInfo['sortNumber'];
1252
        } else {
1253
            // Here we fetch the PID of the record that we point to
1254
            $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

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

1518
                    $propArr = $this->getRecordProperties($table, /** @scrutinizer ignore-type */ $id);
Loading history...
1519
                    $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

1519
                    $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...
1520
                }
1521
                return $res;
1522
            }
1523
            if ($status === 'update') {
1524
                // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1525
                $onlyAllowedTables = $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1526
                if ($onlyAllowedTables) {
1527
                    // use the real page id (default language)
1528
                    $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

1528
                    $recordId = $this->getDefaultLanguagePageId(/** @scrutinizer ignore-type */ $id);
Loading history...
1529
                    $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

1529
                    $theWrongTables = $this->doesPageHaveUnallowedTables($recordId, /** @scrutinizer ignore-type */ $value);
Loading history...
1530
                    if ($theWrongTables) {
1531
                        if ($this->enableLogging) {
1532
                            $propArr = $this->getRecordProperties($table, $id);
1533
                            $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']);
1534
                        }
1535
                        return $res;
1536
                    }
1537
                }
1538
            }
1539
        }
1540
1541
        $curValue = null;
1542
        if ((int)$id !== 0) {
1543
            // Get current value:
1544
            $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

1544
            $curValueRec = $this->recordInfo($table, /** @scrutinizer ignore-type */ $id, $field);
Loading history...
1545
            // isset() won't work here, since values can be NULL
1546
            if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1547
                $curValue = $curValueRec[$field];
1548
            }
1549
        }
1550
1551
        if ($table === 'be_users'
1552
            && ($field === 'admin' || $field === 'password')
1553
            && $status === 'update'
1554
        ) {
1555
            // Do not allow a non system maintainer admin to change admin flag and password of system maintainers
1556
            $systemMaintainers = array_map('intval', $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
1557
            // False if current user is not in system maintainer list or if switch to user mode is active
1558
            $isCurrentUserSystemMaintainer = $this->BE_USER->isSystemMaintainer();
1559
            $isTargetUserInSystemMaintainerList = in_array((int)$id, $systemMaintainers, true);
1560
            if ($field === 'admin') {
1561
                $isFieldChanged = (int)$curValueRec[$field] !== (int)$value;
1562
            } else {
1563
                $isFieldChanged = $curValueRec[$field] !== $value;
1564
            }
1565
            if (!$isCurrentUserSystemMaintainer && $isTargetUserInSystemMaintainerList && $isFieldChanged) {
1566
                $value = $curValueRec[$field];
1567
                $message = GeneralUtility::makeInstance(
1568
                    FlashMessage::class,
1569
                    $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.adminCanNotChangeSystemMaintainer'),
1570
                    '',
1571
                    FlashMessage::ERROR,
1572
                    true
1573
                );
1574
                $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1575
                $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
1576
            }
1577
        }
1578
1579
        // Getting config for the field
1580
        $tcaFieldConf = $this->resolveFieldConfigurationAndRespectColumnsOverrides($table, $field);
1581
1582
        // Create $recFID only for those types that need it
1583
        if ($tcaFieldConf['type'] === 'flex') {
1584
            $recFID = $table . ':' . $id . ':' . $field;
1585
        } else {
1586
            $recFID = null;
1587
        }
1588
1589
        // Perform processing:
1590
        $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

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

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

2804
                $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, /** @scrutinizer ignore-type */ $prep);
Loading history...
2805
                if ($oldRelations != $newRelations) {
2806
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
2807
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
2808
                } else {
2809
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
2810
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
2811
                }
2812
            } else {
2813
                $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
2814
            }
2815
            $valueArray = $dbAnalysis->countItems();
2816
        } else {
2817
            $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

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

3241
        BackendUtility::workspaceOL($table, /** @scrutinizer ignore-type */ $row, $this->BE_USER->workspace);
Loading history...
3242
        $row = BackendUtility::purgeComputedPropertiesFromRecord($row);
3243
3244
        // Initializing:
3245
        $theNewID = StringUtility::getUniqueId('NEW');
3246
        $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3247
        $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3248
        // Getting "copy-after" fields if applicable:
3249
        $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

3249
        $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, /** @scrutinizer ignore-type */ abs($destPid), 0) : [];
Loading history...
3250
        // Page TSconfig related:
3251
        $TSConfig = BackendUtility::getPagesTSconfig($tscPID)['TCEMAIN.'] ?? [];
3252
        $tE = $this->getTableEntries($table, $TSConfig);
3253
        // Traverse ALL fields of the selected record:
3254
        foreach ($row as $field => $value) {
3255
            if (!in_array($field, $nonFields, true)) {
3256
                // Get TCA configuration for the field:
3257
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3258
                // Preparation/Processing of the value:
3259
                // "pid" is hardcoded of course:
3260
                // isset() won't work here, since values can be NULL in each of the arrays
3261
                // except setDefaultOnCopyArray, since we exploded that from a string
3262
                if ($field === 'pid') {
3263
                    $value = $destPid;
3264
                } elseif (array_key_exists($field, $overrideValues)) {
3265
                    // Override value...
3266
                    $value = $overrideValues[$field];
3267
                } elseif (array_key_exists($field, $copyAfterFields)) {
3268
                    // Copy-after value if available:
3269
                    $value = $copyAfterFields[$field];
3270
                } else {
3271
                    // Hide at copy may override:
3272
                    if ($first && $field == $enableField && $GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] && !$this->neverHideAtCopy && !$tE['disableHideAtCopy']) {
3273
                        $value = 1;
3274
                    }
3275
                    // Prepend label on copy:
3276
                    if ($first && $field == $headerField && $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] && !$tE['disablePrependAtCopy']) {
3277
                        $value = $this->getCopyHeader($table, $this->resolvePid($table, $destPid), $field, $this->clearPrefixFromValue($table, $value), 0);
3278
                    }
3279
                    // Processing based on the TCA config field type (files, references, flexforms...)
3280
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $tscPID, $language);
3281
                }
3282
                // Add value to array.
3283
                $data[$table][$theNewID][$field] = $value;
3284
            }
3285
        }
3286
        // Overriding values:
3287
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3288
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3289
        }
3290
        // Setting original UID:
3291
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3292
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3293
        }
3294
        // Do the copy by simply submitting the array through DataHandler:
3295
        /** @var DataHandler $copyTCE */
3296
        $copyTCE = $this->getLocalTCE();
3297
        $copyTCE->start($data, [], $this->BE_USER);
3298
        $copyTCE->process_datamap();
3299
        // Getting the new UID:
3300
        $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID];
3301
        if ($theNewSQLID) {
3302
            $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3303
            // Keep automatically versionized record information:
3304
            if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3305
                $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3306
            }
3307
        }
3308
        $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3309
        unset($copyTCE);
3310
        if (!$ignoreLocalization && $language == 0) {
3311
            //repointing the new translation records to the parent record we just created
3312
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3313
            if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3314
                $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = 0;
3315
            }
3316
            $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
3317
        }
3318
3319
        return $theNewSQLID;
3320
    }
3321
3322
    /**
3323
     * Copying pages
3324
     * Main function for copying pages.
3325
     *
3326
     * @param int $uid Page UID to copy
3327
     * @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
3328
     * @internal should only be used from within DataHandler
3329
     */
3330
    public function copyPages($uid, $destPid)
3331
    {
3332
        // Initialize:
3333
        $uid = (int)$uid;
3334
        $destPid = (int)$destPid;
3335
3336
        $copyTablesAlongWithPage = $this->getAllowedTablesToCopyWhenCopyingAPage();
3337
        // Begin to copy pages if we're allowed to:
3338
        if ($this->admin || in_array('pages', $copyTablesAlongWithPage, true)) {
3339
            // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)
3340
            // This method also copies the localizations of a page
3341
            $theNewRootID = $this->copySpecificPage($uid, $destPid, $copyTablesAlongWithPage, true);
3342
            // If we're going to copy recursively
3343
            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...
3344
                // Get ALL subpages to copy (read-permissions are respected!):
3345
                $CPtable = $this->int_pageTreeInfo([], $uid, (int)$this->copyTree, $theNewRootID);
3346
                // Now copying the subpages:
3347
                foreach ($CPtable as $thePageUid => $thePagePid) {
3348
                    $newPid = $this->copyMappingArray['pages'][$thePagePid];
3349
                    if (isset($newPid)) {
3350
                        $this->copySpecificPage($thePageUid, $newPid, $copyTablesAlongWithPage);
3351
                    } else {
3352
                        $this->log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Something went wrong during copying branch');
3353
                        break;
3354
                    }
3355
                }
3356
            }
3357
        } else {
3358
            $this->log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy page without permission to this table');
3359
        }
3360
    }
3361
3362
    /**
3363
     * Compile a list of tables that should be copied along when a page is about to be copied.
3364
     *
3365
     * First, get the list that the user is allowed to modify (all if admin),
3366
     * and then check against a possible limitation within "DataHandler->copyWhichTables" if not set to "*"
3367
     * to limit the list further down
3368
     *
3369
     * @return array
3370
     */
3371
    protected function getAllowedTablesToCopyWhenCopyingAPage(): array
3372
    {
3373
        // Finding list of tables to copy.
3374
        // These are the tables, the user may modify
3375
        $copyTablesArray = $this->admin ? $this->compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3376
        // If not all tables are allowed then make a list of allowed tables.
3377
        // That is the tables that figure in both allowed tables AND the copyTable-list
3378
        if (strpos($this->copyWhichTables, '*') === false) {
3379
            $definedTablesToCopy = GeneralUtility::trimExplode(',', $this->copyWhichTables, true);
3380
            // Pages are always allowed
3381
            $definedTablesToCopy[] = 'pages';
3382
            $definedTablesToCopy = array_flip($definedTablesToCopy);
3383
            foreach ($copyTablesArray as $k => $table) {
3384
                if (!$table || !isset($definedTablesToCopy[$table])) {
3385
                    unset($copyTablesArray[$k]);
3386
                }
3387
            }
3388
        }
3389
        $copyTablesArray = array_unique($copyTablesArray);
3390
        return $copyTablesArray;
3391
    }
3392
    /**
3393
     * Copying a single page ($uid) to $destPid and all tables in the array copyTablesArray.
3394
     *
3395
     * @param int $uid Page uid
3396
     * @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
3397
     * @param array $copyTablesArray Table on pages to copy along with the page.
3398
     * @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
3399
     * @return int|null The id of the new page, if applicable.
3400
     * @internal should only be used from within DataHandler
3401
     */
3402
    public function copySpecificPage($uid, $destPid, $copyTablesArray, $first = false)
3403
    {
3404
        // Copy the page itself:
3405
        $theNewRootID = $this->copyRecord('pages', $uid, $destPid, $first);
3406
        // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3407
        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...
3408
            foreach ($copyTablesArray as $table) {
3409
                // All records under the page is copied.
3410
                if ($table && is_array($GLOBALS['TCA'][$table]) && $table !== 'pages') {
3411
                    $fields = ['uid'];
3412
                    $languageField = null;
3413
                    $transOrigPointerField = null;
3414
                    $translationSourceField = null;
3415
                    if (BackendUtility::isTableLocalizable($table)) {
3416
                        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
3417
                        $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3418
                        $fields[] = $languageField;
3419
                        $fields[] = $transOrigPointerField;
3420
                        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3421
                            $translationSourceField = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3422
                            $fields[] = $translationSourceField;
3423
                        }
3424
                    }
3425
                    $isTableWorkspaceEnabled = BackendUtility::isTableWorkspaceEnabled($table);
3426
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3427
                    $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
3428
                    $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
3429
                    $queryBuilder
3430
                        ->select(...$fields)
3431
                        ->from($table)
3432
                        ->where(
3433
                            $queryBuilder->expr()->eq(
3434
                                'pid',
3435
                                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3436
                            )
3437
                        );
3438
                    if (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby'])) {
3439
                        $queryBuilder->orderBy($GLOBALS['TCA'][$table]['ctrl']['sortby'], 'DESC');
3440
                    }
3441
                    $queryBuilder->addOrderBy('uid');
3442
                    try {
3443
                        $result = $queryBuilder->execute();
3444
                        $rows = [];
3445
                        while ($row = $result->fetch()) {
3446
                            $rows[$row['uid']] = $row;
3447
                        }
3448
                        // Resolve placeholders of workspace versions
3449
                        if (!empty($rows) && (int)$this->BE_USER->workspace !== 0 && $isTableWorkspaceEnabled) {
3450
                            $rows = array_reverse(
3451
                                $this->resolveVersionedRecords(
3452
                                    $table,
3453
                                    implode(',', $fields),
3454
                                    $GLOBALS['TCA'][$table]['ctrl']['sortby'],
3455
                                    array_keys($rows)
3456
                                ),
3457
                                true
3458
                            );
3459
                        }
3460
                        if (is_array($rows)) {
3461
                            $languageSourceMap = [];
3462
                            $overrideValues = $translationSourceField ? [$translationSourceField => 0] : [];
3463
                            $doRemap = false;
3464
                            foreach ($rows as $row) {
3465
                                // Skip localized records that will be processed in
3466
                                // copyL10nOverlayRecords() on copying the default language record
3467
                                $transOrigPointer = $row[$transOrigPointerField];
3468
                                if ($row[$languageField] > 0 && $transOrigPointer > 0 && isset($rows[$transOrigPointer])) {
3469
                                    continue;
3470
                                }
3471
                                // Copying each of the underlying records...
3472
                                $newUid = $this->copyRecord($table, $row['uid'], $theNewRootID, false, $overrideValues);
3473
                                if ($translationSourceField) {
3474
                                    $languageSourceMap[$row['uid']] = $newUid;
3475
                                    if ($row[$languageField] > 0) {
3476
                                        $doRemap = true;
3477
                                    }
3478
                                }
3479
                            }
3480
                            if ($doRemap) {
3481
                                //remap is needed for records in non-default language records in the "free mode"
3482
                                $this->copy_remapTranslationSourceField($table, $rows, $languageSourceMap);
3483
                            }
3484
                        }
3485
                    } catch (DBALException $e) {
3486
                        $databaseErrorMessage = $e->getPrevious()->getMessage();
3487
                        $this->log($table, $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'An SQL error occurred: ' . $databaseErrorMessage);
3488
                    }
3489
                }
3490
            }
3491
            $this->processRemapStack();
3492
            return $theNewRootID;
3493
        }
3494
        return null;
3495
    }
3496
3497
    /**
3498
     * Copying records, but makes a "raw" copy of a record.
3499
     * Basically the only thing observed is field processing like the copying of files and correction of ids. All other fields are 1-1 copied.
3500
     * 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.
3501
     * 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!?
3502
     * This function is used to create new versions of a record.
3503
     * 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.
3504
     *
3505
     * @param string $table Element table
3506
     * @param int $uid Element UID
3507
     * @param int $pid Element PID (real PID, not checked)
3508
     * @param array $overrideArray Override array - must NOT contain any fields not in the table!
3509
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3510
     * @return int Returns the new ID of the record (if applicable)
3511
     * @internal should only be used from within DataHandler
3512
     */
3513
    public function copyRecord_raw($table, $uid, $pid, $overrideArray = [], array $workspaceOptions = [])
3514
    {
3515
        $uid = (int)$uid;
3516
        // Stop any actions if the record is marked to be deleted:
3517
        // (this can occur if IRRE elements are versionized and child elements are removed)
3518
        if ($this->isElementToBeDeleted($table, $uid)) {
3519
            return null;
3520
        }
3521
        // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3522
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
3523
            return null;
3524
        }
3525
3526
        // Fetch record with permission check
3527
        $row = $this->recordInfoWithPermissionCheck($table, $uid, Permission::PAGE_SHOW);
3528
3529
        // This checks if the record can be selected which is all that a copy action requires.
3530
        if ($row === false) {
3531
            $this->log(
3532
                $table,
3533
                $uid,
3534
                SystemLogDatabaseAction::DELETE,
3535
                0,
3536
                SystemLogErrorClassification::USER_ERROR,
3537
                'Attempt to rawcopy/versionize record which either does not exist or you don\'t have permission to read'
3538
            );
3539
            return null;
3540
        }
3541
3542
        // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3543
        $nonFields = ['uid', 'pid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody'];
3544
3545
        // Merge in override array.
3546
        $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

3546
        $row = array_merge(/** @scrutinizer ignore-type */ $row, $overrideArray);
Loading history...
3547
        // Traverse ALL fields of the selected record:
3548
        foreach ($row as $field => $value) {
3549
            if (!in_array($field, $nonFields, true)) {
3550
                // Get TCA configuration for the field:
3551
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3552
                if (is_array($conf)) {
3553
                    // Processing based on the TCA config field type (files, references, flexforms...)
3554
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3555
                }
3556
                // Add value to array.
3557
                $row[$field] = $value;
3558
            }
3559
        }
3560
        $row['pid'] = $pid;
3561
        // Setting original UID:
3562
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3563
            $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3564
        }
3565
        // Do the copy by internal function
3566
        $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3567
        if ($theNewSQLID) {
3568
            $this->dbAnalysisStoreExec();
3569
            $this->dbAnalysisStore = [];
3570
            return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3571
        }
3572
        return null;
3573
    }
3574
3575
    /**
3576
     * Inserts a record in the database, passing TCA configuration values through checkValue() but otherwise does NOTHING and checks nothing regarding permissions.
3577
     * 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...
3578
     *
3579
     * @param string $table Table name
3580
     * @param array $fieldArray Field array to insert as a record
3581
     * @param int $realPid The value of PID field.
3582
     * @return int Returns the new ID of the record (if applicable)
3583
     * @internal should only be used from within DataHandler
3584
     */
3585
    public function insertNewCopyVersion($table, $fieldArray, $realPid)
3586
    {
3587
        $id = StringUtility::getUniqueId('NEW');
3588
        // $fieldArray is set as current record.
3589
        // 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...
3590
        $this->checkValue_currentRecord = $fieldArray;
3591
        // Makes sure that transformations aren't processed on the copy.
3592
        $backupDontProcessTransformations = $this->dontProcessTransformations;
3593
        $this->dontProcessTransformations = true;
3594
        // Traverse record and input-process each value:
3595
        foreach ($fieldArray as $field => $fieldValue) {
3596
            if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3597
                // Evaluating the value.
3598
                $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0, $fieldArray);
3599
                if (isset($res['value'])) {
3600
                    $fieldArray[$field] = $res['value'];
3601
                }
3602
            }
3603
        }
3604
        // System fields being set:
3605
        if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3606
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3607
        }
3608
        if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3609
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3610
        }
3611
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3612
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3613
        }
3614
        // Finally, insert record:
3615
        $this->insertDB($table, $id, $fieldArray, true);
3616
        // Resets dontProcessTransformations to the previous state.
3617
        $this->dontProcessTransformations = $backupDontProcessTransformations;
3618
        // Return new id:
3619
        return $this->substNEWwithIDs[$id];
3620
    }
3621
3622
    /**
3623
     * Processing/Preparing content for copyRecord() function
3624
     *
3625
     * @param string $table Table name
3626
     * @param int $uid Record uid
3627
     * @param string $field Field name being processed
3628
     * @param string $value Input value to be processed.
3629
     * @param array $row Record array
3630
     * @param array $conf TCA field configuration
3631
     * @param int $realDestPid Real page id (pid) the record is copied to
3632
     * @param int $language Language ID (from sys_language table) used in the duplicated record
3633
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3634
     * @return array|string
3635
     * @internal
3636
     * @see copyRecord()
3637
     */
3638
    public function copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = [])
3639
    {
3640
        $inlineSubType = $this->getInlineFieldType($conf);
3641
        // Get the localization mode for the current (parent) record (keep|select):
3642
        // Register if there are references to take care of or MM is used on an inline field (no change to value):
3643
        if ($this->isReferenceField($conf) || $inlineSubType === 'mm') {
3644
            $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language);
3645
        } elseif ($inlineSubType !== false) {
3646
            $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions);
3647
        }
3648
        // 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())
3649
        if ($conf['type'] === 'flex') {
3650
            // Get current value array:
3651
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
3652
            $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
3653
                ['config' => $conf],
3654
                $table,
3655
                $field,
3656
                $row
3657
            );
3658
            $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
3659
            $currentValueArray = GeneralUtility::xml2array($value);
3660
            // Traversing the XML structure, processing files:
3661
            if (is_array($currentValueArray)) {
3662
                $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
3663
                // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3664
                $value = $currentValueArray;
3665
            }
3666
        }
3667
        return $value;
3668
    }
3669
3670
    /**
3671
     * Processes the children of an MM relation field (select, group, inline) when the parent record is copied.
3672
     *
3673
     * @param string $table
3674
     * @param int $uid
3675
     * @param string $field
3676
     * @param mixed $value
3677
     * @param array $conf
3678
     * @param string $language
3679
     * @return mixed
3680
     */
3681
    protected function copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language)
3682
    {
3683
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
3684
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
3685
        $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3686
        $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3687
        // Localize referenced records of select fields:
3688
        $localizingNonManyToManyFieldReferences = empty($mmTable) && $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3689
        /** @var RelationHandler $dbAnalysis */
3690
        $dbAnalysis = $this->createRelationHandlerInstance();
3691
        $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3692
        $purgeItems = false;
3693
        if ($language > 0 && $localizingNonManyToManyFieldReferences) {
3694
            foreach ($dbAnalysis->itemArray as $index => $item) {
3695
                // Since select fields can reference many records, check whether there's already a localization:
3696
                $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

3696
                $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3697
                if ($recordLocalization) {
3698
                    $dbAnalysis->itemArray[$index]['id'] = $recordLocalization[0]['uid'];
3699
                } elseif ($this->isNestedElementCallRegistered($item['table'], $item['id'], 'localize-' . (string)$language) === false) {
3700
                    $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

3700
                    $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3701
                }
3702
            }
3703
            $purgeItems = true;
3704
        }
3705
3706
        if ($purgeItems || $mmTable) {
3707
            $dbAnalysis->purgeItemArray();
3708
            $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

3708
            $value = implode(',', $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName));
Loading history...
3709
        }
3710
        // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3711
        if ($value) {
3712
            $this->registerDBList[$table][$uid][$field] = $value;
3713
        }
3714
3715
        return $value;
3716
    }
3717
3718
    /**
3719
     * Processes child records in an inline (IRRE) element when the parent record is copied.
3720
     *
3721
     * @param string $table
3722
     * @param int $uid
3723
     * @param string $field
3724
     * @param mixed $value
3725
     * @param array $row
3726
     * @param array $conf
3727
     * @param int $realDestPid
3728
     * @param string $language
3729
     * @param array $workspaceOptions
3730
     * @return string
3731
     */
3732
    protected function copyRecord_processInline(
3733
        $table,
3734
        $uid,
3735
        $field,
3736
        $value,
3737
        $row,
3738
        $conf,
3739
        $realDestPid,
3740
        $language,
3741
        array $workspaceOptions
3742
    ) {
3743
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3744
        /** @var RelationHandler $dbAnalysis */
3745
        $dbAnalysis = $this->createRelationHandlerInstance();
3746
        $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3747
        // Walk through the items, copy them and remember the new id:
3748
        foreach ($dbAnalysis->itemArray as $k => $v) {
3749
            $newId = null;
3750
            // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3751
            if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3752
                // Children should be localized when the parent gets localized the first time, just do it:
3753
                $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

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

3872
        $tscPID = BackendUtility::getTSconfig_pidValue($table, /** @scrutinizer ignore-type */ $uid, $destPid);
Loading history...
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

3872
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, /** @scrutinizer ignore-type */ $destPid);
Loading history...
3873
        // Get the localized records to be copied
3874
        $l10nRecords = $queryBuilder->execute()->fetchAll();
3875
        if (is_array($l10nRecords)) {
3876
            $localizedDestPids = [];
3877
            // If $destPid < 0, then it is the uid of the original language record we are inserting after
3878
            if ($destPid < 0) {
3879
                // Get the localized records of the record we are inserting after
3880
                $queryBuilder->setParameter('pointer', abs($destPid), \PDO::PARAM_INT);
3881
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
3882
                // Index the localized record uids by language
3883
                if (is_array($destL10nRecords)) {
3884
                    foreach ($destL10nRecords as $record) {
3885
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
3886
                    }
3887
                }
3888
            }
3889
            $languageSourceMap = [
3890
                $uid => $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
3891
            ];
3892
            // Copy the localized records after the corresponding localizations of the destination record
3893
            foreach ($l10nRecords as $record) {
3894
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
3895
                if ($localizedDestPid < 0) {
3896
                    $newUid = $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
3897
                } else {
3898
                    $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

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

4083
                [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4084
                $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4085
                // Setting PID
4086
                $updateFields['pid'] = $destPid;
4087
                // Table is sorted by 'sortby'
4088
                if ($sortColumn && !isset($updateFields[$sortColumn])) {
4089
                    $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4090
                    $updateFields[$sortColumn] = $sortNumber;
4091
                }
4092
                // Check for child records that have also to be moved
4093
                $this->moveRecord_procFields($table, $uid, $destPid);
4094
                // Create query for update:
4095
                GeneralUtility::makeInstance(ConnectionPool::class)
4096
                    ->getConnectionForTable($table)
4097
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4098
                // Check for the localizations of that element
4099
                $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4100
                // Call post processing hooks:
4101
                foreach ($hookObjectsArr as $hookObj) {
4102
                    if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4103
                        $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4104
                    }
4105
                }
4106
4107
                $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4108
                if ($this->enableLogging) {
4109
                    // Logging...
4110
                    $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4111
                    if ($destPid != $propArr['pid']) {
4112
                        // Logged to old page
4113
                        $newPropArr = $this->getRecordProperties($table, $uid);
4114
                        $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4115
                        $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']);
4116
                        // Logged to new page
4117
                        $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);
4118
                    } else {
4119
                        // Logged to new page
4120
                        $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);
4121
                    }
4122
                }
4123
                // Clear cache after moving
4124
                $this->registerRecordIdForPageCacheClearing($table, $uid);
4125
                $this->fixUniqueInPid($table, $uid);
4126
                $this->fixUniqueInSite($table, (int)$uid);
4127
                if ($table === 'pages') {
4128
                    $this->fixUniqueInSiteForSubpages((int)$uid);
4129
                }
4130
            } elseif ($this->enableLogging) {
4131
                $destPropArr = $this->getRecordProperties('pages', $destPid);
4132
                $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']);
4133
            }
4134
        } elseif ($sortColumn) {
4135
            // Put after another record
4136
            // Table is being sorted
4137
            // Save the position to which the original record is requested to be moved
4138
            $originalRecordDestinationPid = $destPid;
4139
            $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4140
            // Setting the destPid to the new pid of the record.
4141
            $destPid = $sortInfo['pid'];
4142
            // If not an array, there was an error (which is already logged)
4143
            if (is_array($sortInfo)) {
4144
                if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4145
                    // clear cache before moving
4146
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4147
                    // We now update the pid and sortnumber (if not set for page translations)
4148
                    $updateFields['pid'] = $destPid;
4149
                    if (!isset($updateFields[$sortColumn])) {
4150
                        $updateFields[$sortColumn] = $sortInfo['sortNumber'];
4151
                    }
4152
                    // Check for child records that have also to be moved
4153
                    $this->moveRecord_procFields($table, $uid, $destPid);
4154
                    // Create query for update:
4155
                    GeneralUtility::makeInstance(ConnectionPool::class)
4156
                        ->getConnectionForTable($table)
4157
                        ->update($table, $updateFields, ['uid' => (int)$uid]);
4158
                    // Check for the localizations of that element
4159
                    $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4160
                    // Call post processing hooks:
4161
                    foreach ($hookObjectsArr as $hookObj) {
4162
                        if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4163
                            $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4164
                        }
4165
                    }
4166
                    $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4167
                    if ($this->enableLogging) {
4168
                        // Logging...
4169
                        $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4170
                        if ($destPid != $propArr['pid']) {
4171
                            // Logged to old page
4172
                            $newPropArr = $this->getRecordProperties($table, $uid);
4173
                            $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4174
                            $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']);
4175
                            // Logged to old page
4176
                            $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);
4177
                        } else {
4178
                            // Logged to old page
4179
                            $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);
4180
                        }
4181
                    }
4182
                    // Clear cache after moving
4183
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4184
                    $this->fixUniqueInPid($table, $uid);
4185
                    $this->fixUniqueInSite($table, (int)$uid);
4186
                    if ($table === 'pages') {
4187
                        $this->fixUniqueInSiteForSubpages((int)$uid);
4188
                    }
4189
                } elseif ($this->enableLogging) {
4190
                    $destPropArr = $this->getRecordProperties('pages', $destPid);
4191
                    $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']);
4192
                }
4193
            } else {
4194
                $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']);
4195
            }
4196
        }
4197
    }
4198
4199
    /**
4200
     * Walk through all fields of the moved record and look for children of e.g. the inline type.
4201
     * If child records are found, they are also move to the new $destPid.
4202
     *
4203
     * @param string $table Record Table
4204
     * @param int $uid Record UID
4205
     * @param int $destPid Position to move to
4206
     * @internal should only be used from within DataHandler
4207
     */
4208
    public function moveRecord_procFields($table, $uid, $destPid)
4209
    {
4210
        $row = BackendUtility::getRecordWSOL($table, $uid);
4211
        if (is_array($row) && (int)$destPid !== (int)$row['pid']) {
4212
            $conf = $GLOBALS['TCA'][$table]['columns'];
4213
            foreach ($row as $field => $value) {
4214
                $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4215
            }
4216
        }
4217
    }
4218
4219
    /**
4220
     * Move child records depending on the field type of the parent record.
4221
     *
4222
     * @param string $table Record Table
4223
     * @param string $uid Record UID
4224
     * @param string $destPid Position to move to
4225
     * @param string $field Record field
4226
     * @param string $value Record field value
4227
     * @param array $conf TCA configuration of current field
4228
     * @internal should only be used from within DataHandler
4229
     */
4230
    public function moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
4231
    {
4232
        $dbAnalysis = null;
4233
        if ($conf['type'] === 'inline') {
4234
            $foreign_table = $conf['foreign_table'];
4235
            $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4236
            if ($foreign_table && $moveChildrenWithParent) {
4237
                $inlineType = $this->getInlineFieldType($conf);
4238
                if ($inlineType === 'list' || $inlineType === 'field') {
4239
                    if ($table === 'pages') {
4240
                        // If the inline elements are related to a page record,
4241
                        // make sure they reside at that page and not at its parent
4242
                        $destPid = $uid;
4243
                    }
4244
                    $dbAnalysis = $this->createRelationHandlerInstance();
4245
                    $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

4245
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
4246
                }
4247
            }
4248
        }
4249
        // Move the records
4250
        if (isset($dbAnalysis)) {
4251
            // Moving records to a positive destination will insert each
4252
            // record at the beginning, thus the order is reversed here:
4253
            foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4254
                $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

4254
                $this->moveRecord($v['table'], $v['id'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4255
            }
4256
        }
4257
    }
4258
4259
    /**
4260
     * Find l10n-overlay records and perform the requested move action for these records.
4261
     *
4262
     * @param string $table Record Table
4263
     * @param string $uid Record UID
4264
     * @param string $destPid Position to move to
4265
     * @param string $originalRecordDestinationPid Position to move the original record to
4266
     * @internal should only be used from within DataHandler
4267
     */
4268
    public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4269
    {
4270
        // There's no need to perform this for non-localizable tables
4271
        if (!BackendUtility::isTableLocalizable($table)) {
4272
            return;
4273
        }
4274
4275
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4276
        $queryBuilder->getRestrictions()
4277
            ->removeAll()
4278
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4279
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
4280
4281
        $queryBuilder->select('*')
4282
            ->from($table)
4283
            ->where(
4284
                $queryBuilder->expr()->eq(
4285
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
4286
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
4287
                )
4288
            );
4289
4290
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
4291
            $queryBuilder->andWhere(
4292
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
4293
            );
4294
        }
4295
4296
        $l10nRecords = $queryBuilder->execute()->fetchAll();
4297
        if (is_array($l10nRecords)) {
4298
            $localizedDestPids = [];
4299
            // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4300
            if ($originalRecordDestinationPid < 0) {
4301
                // Get the localized records of the record we are inserting after
4302
                $queryBuilder->setParameter('pointer', abs($originalRecordDestinationPid), \PDO::PARAM_INT);
4303
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
4304
                // Index the localized record uids by language
4305
                if (is_array($destL10nRecords)) {
4306
                    foreach ($destL10nRecords as $record) {
4307
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4308
                    }
4309
                }
4310
            }
4311
            // Move the localized records after the corresponding localizations of the destination record
4312
            foreach ($l10nRecords as $record) {
4313
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4314
                if ($localizedDestPid < 0) {
4315
                    $this->moveRecord($table, $record['uid'], $localizedDestPid);
4316
                } else {
4317
                    $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

4317
                    $this->moveRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4318
                }
4319
            }
4320
        }
4321
    }
4322
4323
    /**
4324
     * Localizes a record to another system language
4325
     *
4326
     * @param string $table Table name
4327
     * @param int $uid Record uid (to be localized)
4328
     * @param int $language Language ID (from sys_language table)
4329
     * @return int|bool The uid (int) of the new translated record or FALSE (bool) if something went wrong
4330
     * @internal should only be used from within DataHandler
4331
     */
4332
    public function localize($table, $uid, $language)
4333
    {
4334
        $newId = false;
4335
        $uid = (int)$uid;
4336
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize-' . (string)$language) !== false) {
4337
            return false;
4338
        }
4339
4340
        $this->registerNestedElementCall($table, $uid, 'localize-' . (string)$language);
4341
        if (!$GLOBALS['TCA'][$table]['ctrl']['languageField'] || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
4342
            $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table ' . $table, SystemLogErrorClassification::USER_ERROR);
4343
            return false;
4344
        }
4345
        $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4346
        if (!$langRec) {
4347
            $this->newlog('Sys language UID "' . $language . '" not found valid!', SystemLogErrorClassification::USER_ERROR);
4348
            return false;
4349
        }
4350
4351
        if (!$this->doesRecordExist($table, $uid, Permission::PAGE_SHOW)) {
4352
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' without permission.', SystemLogErrorClassification::USER_ERROR);
4353
            return false;
4354
        }
4355
4356
        // Getting workspace overlay if possible - this will localize versions in workspace if any
4357
        $row = BackendUtility::getRecordWSOL($table, $uid);
4358
        if (!is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
4359
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' that did not exist!', SystemLogErrorClassification::USER_ERROR);
4360
            return false;
4361
        }
4362
4363
        // Make sure that records which are translated from another language than the default language have a correct
4364
        // localization source set themselves, before translating them to another language.
4365
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4366
            && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0) {
4367
            $localizationParentRecord = BackendUtility::getRecord(
4368
                $table,
4369
                $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4370
            );
4371
            if ((int)$localizationParentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4372
                $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);
4373
                return false;
4374
            }
4375
        }
4376
4377
        // Default language records must never have a localization parent as they are the origin of any translation.
4378
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4379
            && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4380
            $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);
4381
            return false;
4382
        }
4383
4384
        $recordLocalizations = BackendUtility::getRecordLocalization($table, $uid, $language, 'AND pid=' . (int)$row['pid']);
4385
4386
        if (!empty($recordLocalizations)) {
4387
            $this->newlog(sprintf(
4388
                'Localization failed: there already are localizations (%s) for language %d of the "%s" record %d!',
4389
                implode(', ', array_column($recordLocalizations, 'uid')),
4390
                $language,
4391
                $table,
4392
                $uid
4393
            ), 1);
4394
            return false;
4395
        }
4396
4397
        // Initialize:
4398
        $overrideValues = [];
4399
        // Set override values:
4400
        $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $langRec['uid'];
4401
        // If the translated record is a default language record, set it's uid as localization parent of the new record.
4402
        // If translating from any other language, no override is needed; we just can copy the localization parent of
4403
        // the original record (which is pointing to the correspondent default language record) to the new record.
4404
        // In copy / free mode the TransOrigPointer field is always set to 0, as no connection to the localization parent is wanted in that case.
4405
        // For pages, there is no "copy/free mode".
4406
        if (($this->useTransOrigPointerField || $table === 'pages') && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4407
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $uid;
4408
        } elseif (!$this->useTransOrigPointerField) {
4409
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = 0;
4410
        }
4411
        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
4412
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = $uid;
4413
        }
4414
        // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4415
        if (isset($GLOBALS['TCA'][$table]['ctrl']['type'])) {
4416
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4417
        }
4418
        // Set exclude Fields:
4419
        foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fCfg) {
4420
            $translateToMsg = '';
4421
            // Check if we are just prefixing:
4422
            if ($fCfg['l10n_mode'] === 'prefixLangTitle') {
4423
                if (($fCfg['config']['type'] === 'text' || $fCfg['config']['type'] === 'input') && (string)$row[$fN] !== '') {
4424
                    [$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

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

4787
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4788
        $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4789
        $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
4790
        $databaseErrorMessage = '';
4791
        if ($deleteField && !$forceHardDelete) {
4792
            $updateFields = [
4793
                $deleteField => $undeleteRecord ? 0 : 1
4794
            ];
4795
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4796
                $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4797
            }
4798
            // before (un-)deleting this record, check for child records or references
4799
            $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
4800
            try {
4801
                // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
4802
                if (!$undeleteRecord) {
4803
                    $this->deletedRecords[$table][] = (int)$uid;
4804
                    $this->deleteL10nOverlayRecords($table, $uid);
4805
                }
4806
                GeneralUtility::makeInstance(ConnectionPool::class)
4807
                    ->getConnectionForTable($table)
4808
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4809
            } catch (DBALException $e) {
4810
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4811
            }
4812
        } else {
4813
            // Delete the hard way...:
4814
            try {
4815
                $this->hardDeleteSingleRecord($table, (int)$uid);
4816
                $this->deletedRecords[$table][] = (int)$uid;
4817
                $this->deleteL10nOverlayRecords($table, $uid);
4818
            } catch (DBALException $e) {
4819
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4820
            }
4821
        }
4822
        if ($this->enableLogging) {
4823
            $state = $undeleteRecord ? SystemLogDatabaseAction::INSERT : SystemLogDatabaseAction::DELETE;
4824
            if ($databaseErrorMessage === '') {
4825
                if ($forceHardDelete) {
4826
                    $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
4827
                } else {
4828
                    $message = $state === 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
4829
                }
4830
                $propArr = $this->getRecordProperties($table, $uid);
4831
                $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4832
4833
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::MESSAGE, $message, 0, [
4834
                    $propArr['header'],
4835
                    $table . ':' . $uid,
4836
                    $pagePropArr['header'],
4837
                    $propArr['pid']
4838
                ], $propArr['event_pid']);
4839
            } else {
4840
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::TODAYS_SPECIAL, $databaseErrorMessage);
4841
            }
4842
        }
4843
4844
        // Add history entry
4845
        if ($undeleteRecord) {
4846
            $this->getRecordHistoryStore()->undeleteRecord($table, $uid, $this->correlationId);
4847
        } else {
4848
            $this->getRecordHistoryStore()->deleteRecord($table, $uid, $this->correlationId);
4849
        }
4850
4851
        // Update reference index:
4852
        $this->updateRefIndex($table, $uid);
4853
4854
        // We track calls to update the reference index as to avoid calling it twice
4855
        // with the same arguments. This is done because reference indexing is quite
4856
        // costly and the update reference index stack usually contain duplicates.
4857
        // NB: also filled and checked in loop below. The initialisation prevents
4858
        // running the "root" record twice if it appears in the stack twice.
4859
        $updateReferenceIndexCalls = [[$table, $uid]];
4860
4861
        // If there are entries in the updateRefIndexStack
4862
        if (is_array($this->updateRefIndexStack[$table]) && is_array($this->updateRefIndexStack[$table][$uid])) {
4863
            while ($args = array_pop($this->updateRefIndexStack[$table][$uid])) {
4864
                if (!in_array($args, $updateReferenceIndexCalls, true)) {
4865
                    // $args[0]: table, $args[1]: uid
4866
                    $this->updateRefIndex($args[0], $args[1]);
4867
                    $updateReferenceIndexCalls[] = $args;
4868
                }
4869
            }
4870
            unset($this->updateRefIndexStack[$table][$uid]);
4871
        }
4872
    }
4873
4874
    /**
4875
     * Used to delete page because it will check for branch below pages and disallowed tables on the page as well.
4876
     *
4877
     * @param int $uid Page id
4878
     * @param bool $force If TRUE, pages are not checked for permission.
4879
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4880
     * @param bool $deleteRecordsOnPage If false, records on the page will not be deleted (edge case while swapping workspaces)
4881
     * @internal should only be used from within DataHandler
4882
     */
4883
    public function deletePages($uid, $force = false, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4884
    {
4885
        $uid = (int)$uid;
4886
        if ($uid === 0) {
4887
            if ($this->enableLogging) {
4888
                $this->log('pages', $uid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'Deleting all pages starting from the root-page is disabled.', -1, [], 0);
4889
            }
4890
            return;
4891
        }
4892
        // Getting list of pages to delete:
4893
        if ($force) {
4894
            // Returns the branch WITHOUT permission checks (0 secures that), so it cannot return -1
4895
            $pageIdsInBranch = $this->doesBranchExist('', $uid, 0, true);
4896
            $res = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
4897
        } else {
4898
            $res = $this->canDeletePage($uid);
4899
        }
4900
        // Perform deletion if not error:
4901
        if (is_array($res)) {
4902
            foreach ($res as $deleteId) {
4903
                $this->deleteSpecificPage($deleteId, $forceHardDelete, $deleteRecordsOnPage);
4904
            }
4905
        } else {
4906
            /** @var FlashMessage $flashMessage */
4907
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
4908
            /** @var FlashMessageService $flashMessageService */
4909
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
4910
            $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
4911
            $this->newlog($res, SystemLogErrorClassification::USER_ERROR);
4912
        }
4913
    }
4914
4915
    /**
4916
     * Delete a page (or set deleted field to 1) and all records on it.
4917
     *
4918
     * @param int $uid Page id
4919
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4920
     * @param bool $deleteRecordsOnPage If false, records on the page will not be deleted (edge case while swapping workspaces)
4921
     * @internal
4922
     * @see deletePages()
4923
     */
4924
    public function deleteSpecificPage($uid, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4925
    {
4926
        $uid = (int)$uid;
4927
        if (!$uid) {
4928
            // Early void return on invalid uid
4929
            return;
4930
        }
4931
        $forceHardDelete = (bool)$forceHardDelete;
4932
4933
        // Delete either a default language page or a translated page
4934
        $pageIdInDefaultLanguage = $this->getDefaultLanguagePageId($uid);
4935
        $isPageTranslation = false;
4936
        $pageLanguageId = 0;
4937
        if ($pageIdInDefaultLanguage !== $uid) {
4938
            // For translated pages, translated records in other tables (eg. tt_content) for the
4939
            // to-delete translated page have their pid field set to the uid of the default language record,
4940
            // NOT the uid of the translated page record.
4941
            // If a translated page is deleted, only translations of records in other tables of this language
4942
            // should be deleted. The code checks if the to-delete page is a translated page and
4943
            // adapts the query for other tables to use the uid of the default language page as pid together
4944
            // with the language id of the translated page.
4945
            $isPageTranslation = true;
4946
            $pageLanguageId = $this->pageInfo($uid, $GLOBALS['TCA']['pages']['ctrl']['languageField']);
4947
        }
4948
4949
        if ($deleteRecordsOnPage) {
4950
            $tableNames = $this->compileAdminTables();
4951
            foreach ($tableNames as $table) {
4952
                if ($table === 'pages' || ($isPageTranslation && !BackendUtility::isTableLocalizable($table))) {
4953
                    // Skip pages table. And skip table if not translatable, but a translated page is deleted
4954
                    continue;
4955
                }
4956
4957
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4958
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
4959
                $queryBuilder
4960
                    ->select('uid')
4961
                    ->from($table);
4962
4963
                if ($isPageTranslation) {
4964
                    // Only delete records in the specified language
4965
                    $queryBuilder->where(
4966
                        $queryBuilder->expr()->eq(
4967
                            'pid',
4968
                            $queryBuilder->createNamedParameter($pageIdInDefaultLanguage, \PDO::PARAM_INT)
4969
                        ),
4970
                        $queryBuilder->expr()->eq(
4971
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
4972
                            $queryBuilder->createNamedParameter($pageLanguageId, \PDO::PARAM_INT)
4973
                        )
4974
                    );
4975
                } else {
4976
                    // Delete all records on this page
4977
                    $queryBuilder->where(
4978
                        $queryBuilder->expr()->eq(
4979
                            'pid',
4980
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
4981
                        )
4982
                    );
4983
                }
4984
                $statement = $queryBuilder->execute();
4985
4986
                while ($row = $statement->fetch()) {
4987
                    // Handle a detail related to workspace placeholder records, delete any
4988
                    // further workspace overlays for the record in question, then delete the record.
4989
                    $this->copyMovedRecordToNewLocation($table, $row['uid']);
4990
                    $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
4991
                    $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
4992
                }
4993
            }
4994
        }
4995
4996
        // Handle a detail related to workspace placeholder records, delete any
4997
        // further workspace overlays for the page in question, then delete the page.
4998
        $this->copyMovedRecordToNewLocation('pages', $uid);
4999
        $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
5000
        $this->deleteRecord('pages', $uid, true, $forceHardDelete);
5001
    }
5002
5003
    /**
5004
     * Copies the move placeholder of a record to its new location (pid).
5005
     * This will create a "new" placeholder at the new location and
5006
     * a version for this new placeholder. The original move placeholder
5007
     * is then deleted because it is not needed anymore.
5008
     *
5009
     * This method is used to assure that moved records are not deleted
5010
     * when the origin page is deleted.
5011
     *
5012
     * @param string $table Record table
5013
     * @param int $uid Record uid
5014
     */
5015
    protected function copyMovedRecordToNewLocation($table, $uid)
5016
    {
5017
        if ($this->BE_USER->workspace > 0) {
5018
            $originalRecord = BackendUtility::getRecord($table, $uid);
5019
            $movePlaceholder = BackendUtility::getMovePlaceholder($table, $uid);
5020
            // Check whether target page to copied to is different to current page
5021
            // Cloning on the same page is superfluous and does not help at all
5022
            if (!empty($originalRecord) && !empty($movePlaceholder) && (int)$originalRecord['pid'] !== (int)$movePlaceholder['pid']) {
5023
                // If move placeholder exists, copy to new location
5024
                // This will create a New placeholder on the new location
5025
                // and a version for this new placeholder
5026
                $command = [
5027
                    $table => [
5028
                        $uid => [
5029
                            'copy' => '-' . $movePlaceholder['uid']
5030
                        ]
5031
                    ]
5032
                ];
5033
                /** @var DataHandler $dataHandler */
5034
                $dataHandler = GeneralUtility::makeInstance(__CLASS__);
5035
                $dataHandler->enableLogging = $this->enableLogging;
5036
                $dataHandler->neverHideAtCopy = true;
5037
                $dataHandler->start([], $command, $this->BE_USER);
5038
                $dataHandler->process_cmdmap();
5039
                unset($dataHandler);
5040
5041
                // Delete move placeholder
5042
                $this->deleteRecord($table, $movePlaceholder['uid'], true, true);
5043
            }
5044
        }
5045
    }
5046
5047
    /**
5048
     * Used to evaluate if a page can be deleted
5049
     *
5050
     * @param int $uid Page id
5051
     * @return int[]|string If array: List of page uids to traverse and delete (means OK), if string: error message.
5052
     * @internal should only be used from within DataHandler
5053
     */
5054
    public function canDeletePage($uid)
5055
    {
5056
        $uid = (int)$uid;
5057
        $isTranslatedPage = null;
5058
5059
        // If we may at all delete this page
5060
        // If this is a page translation, do the check against the perms_* of the default page
5061
        // Because it is currently only deleting the translation
5062
        $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
5063
        if ($defaultLanguagePageId !== $uid) {
5064
            if ($this->doesRecordExist('pages', (int)$defaultLanguagePageId, Permission::PAGE_DELETE)) {
5065
                $isTranslatedPage = true;
5066
            } else {
5067
                return 'Attempt to delete page without permissions';
5068
            }
5069
        } elseif (!$this->doesRecordExist('pages', $uid, Permission::PAGE_DELETE)) {
5070
            return 'Attempt to delete page without permissions';
5071
        }
5072
5073
        $pageIdsInBranch = $this->doesBranchExist('', $uid, Permission::PAGE_DELETE, true);
5074
5075
        if ($this->deleteTree) {
5076
            if ($pageIdsInBranch === -1) {
5077
                return 'Attempt to delete pages in branch without permissions';
5078
            }
5079
5080
            $pagesInBranch = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
5081
        } else {
5082
            if ($pageIdsInBranch === -1) {
5083
                return 'Attempt to delete page without permissions';
5084
            }
5085
            if ($pageIdsInBranch !== '') {
5086
                return 'Attempt to delete page which has subpages';
5087
            }
5088
5089
            $pagesInBranch = [$uid];
5090
        }
5091
5092
        if ($disallowedTables = $this->checkForRecordsFromDisallowedTables($pagesInBranch)) {
5093
            return 'Attempt to delete records from disallowed tables (' . implode(', ', $disallowedTables) . ')';
5094
        }
5095
5096
        foreach ($pagesInBranch as $pageInBranch) {
5097
            if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, $isTranslatedPage ? false : true)) {
5098
                return 'Attempt to delete page which has prohibited localizations.';
5099
            }
5100
        }
5101
        return $pagesInBranch;
5102
    }
5103
5104
    /**
5105
     * Returns TRUE if record CANNOT be deleted, otherwise FALSE. Used to check before the versioning API allows a record to be marked for deletion.
5106
     *
5107
     * @param string $table Record Table
5108
     * @param int $id Record UID
5109
     * @return string Returns a string IF there is an error (error string explaining). FALSE means record can be deleted
5110
     * @internal should only be used from within DataHandler
5111
     */
5112
    public function cannotDeleteRecord($table, $id)
5113
    {
5114
        if ($table === 'pages') {
5115
            $res = $this->canDeletePage($id);
5116
            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...
5117
        }
5118
        if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
5119
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
5120
            $perms = Permission::PAGE_EDIT;
5121
        } else {
5122
            $perms = Permission::CONTENT_EDIT;
5123
        }
5124
        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...
5125
    }
5126
5127
    /**
5128
     * Determines whether a record can be undeleted.
5129
     *
5130
     * @param string $table Table name of the record
5131
     * @param int $uid uid of the record
5132
     * @return bool Whether the record can be undeleted
5133
     * @internal should only be used from within DataHandler
5134
     */
5135
    public function isRecordUndeletable($table, $uid)
5136
    {
5137
        $result = false;
5138
        $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5139
        if ($record['pid']) {
5140
            $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5141
            // The page containing the record is not deleted, thus the record can be undeleted:
5142
            if (!$page['deleted']) {
5143
                $result = true;
5144
            } else {
5145
                $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

5145
                $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...
5146
            }
5147
        } else {
5148
            // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5149
            $result = true;
5150
        }
5151
        return $result;
5152
    }
5153
5154
    /**
5155
     * Before a record is deleted, check if it has references such as inline type or MM references.
5156
     * If so, set these child records also to be deleted.
5157
     *
5158
     * @param string $table Record Table
5159
     * @param string $uid Record UID
5160
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5161
     * @see deleteRecord()
5162
     * @internal should only be used from within DataHandler
5163
     */
5164
    public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
5165
    {
5166
        $conf = $GLOBALS['TCA'][$table]['columns'];
5167
        $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

5167
        $row = BackendUtility::getRecord($table, /** @scrutinizer ignore-type */ $uid, '*', '', false);
Loading history...
5168
        if (empty($row)) {
5169
            return;
5170
        }
5171
        foreach ($row as $field => $value) {
5172
            $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5173
        }
5174
    }
5175
5176
    /**
5177
     * Process fields of a record to be deleted and search for special handling, like
5178
     * inline type, MM records, etc.
5179
     *
5180
     * @param string $table Record Table
5181
     * @param string $uid Record UID
5182
     * @param string $field Record field
5183
     * @param string $value Record field value
5184
     * @param array $conf TCA configuration of current field
5185
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5186
     * @see deleteRecord()
5187
     * @internal should only be used from within DataHandler
5188
     */
5189
    public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
5190
    {
5191
        if ($conf['type'] === 'inline') {
5192
            $foreign_table = $conf['foreign_table'];
5193
            if ($foreign_table) {
5194
                $inlineType = $this->getInlineFieldType($conf);
5195
                if ($inlineType === 'list' || $inlineType === 'field') {
5196
                    /** @var RelationHandler $dbAnalysis */
5197
                    $dbAnalysis = $this->createRelationHandlerInstance();
5198
                    $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

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

5682
                    if (!empty($dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName))) {
Loading history...
5683
                        $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5684
                    }
5685
                    /** @var RelationHandler $dbAnalysis */
5686
                    $dbAnalysis = $this->createRelationHandlerInstance();
5687
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5688
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
5689
                        $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5690
                    }
5691
                }
5692
            } elseif ($conf['type'] === 'flex') {
5693
                // Current record
5694
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5695
                    $fConf,
5696
                    $table,
5697
                    $field,
5698
                    $currentRec
5699
                );
5700
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5701
                $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5702
                if (is_array($currentValueArray)) {
5703
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5704
                }
5705
                // Swap record
5706
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5707
                    $fConf,
5708
                    $table,
5709
                    $field,
5710
                    $swapRec
5711
                );
5712
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5713
                $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5714
                if (is_array($currentValueArray)) {
5715
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5716
                }
5717
            }
5718
        }
5719
        // Execute:
5720
        $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5721
    }
5722
5723
    /**
5724
     * Callback function for traversing the FlexForm structure in relation to ...
5725
     *
5726
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
5727
     * @param array $dsConf TCA field configuration (from Data Structure XML)
5728
     * @param string $dataValue The value of the flexForm field
5729
     * @param string $dataValue_ext1 Not used.
5730
     * @param string $dataValue_ext2 Not used.
5731
     * @param string $path Path in flexforms
5732
     * @see version_remapMMForVersionSwap()
5733
     * @see checkValue_flex_procInData_travDS()
5734
     * @internal should only be used from within DataHandler
5735
     */
5736
    public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
5737
    {
5738
        // Extract parameters:
5739
        [$table, $uid, $field] = $pParams;
5740
        if ($this->isReferenceField($dsConf)) {
5741
            $allowedTables = $dsConf['type'] === 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5742
            $prependName = $dsConf['type'] === 'group' ? $dsConf['prepend_tname'] : '';
5743
            if ($dsConf['MM']) {
5744
                /** @var RelationHandler $dbAnalysis */
5745
                $dbAnalysis = $this->createRelationHandlerInstance();
5746
                $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5747
                $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5748
            }
5749
        }
5750
    }
5751
5752
    /**
5753
     * Performing the remapping operations found necessary in version_remapMMForVersionSwap()
5754
     * 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.
5755
     *
5756
     * @param string $table Table for the two input records
5757
     * @param int $id Current record (about to go offline)
5758
     * @param int $swapWith Swap record (about to go online)
5759
     * @see version_remapMMForVersionSwap()
5760
     * @internal should only be used from within DataHandler
5761
     */
5762
    public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
5763
    {
5764
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5765
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5766
                $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5767
            }
5768
        }
5769
        if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5770
            foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5771
                $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5772
            }
5773
        }
5774
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5775
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5776
                $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5777
            }
5778
        }
5779
    }
5780
5781
    /*********************************************
5782
     *
5783
     * Cmd: Helper functions
5784
     *
5785
     ********************************************/
5786
5787
    /**
5788
     * Returns an instance of DataHandler for handling local datamaps/cmdmaps
5789
     *
5790
     * @return DataHandler
5791
     */
5792
    protected function getLocalTCE()
5793
    {
5794
        $copyTCE = GeneralUtility::makeInstance(DataHandler::class);
5795
        $copyTCE->copyTree = $this->copyTree;
5796
        $copyTCE->enableLogging = $this->enableLogging;
5797
        // Transformations should NOT be carried out during copy
5798
        $copyTCE->dontProcessTransformations = true;
5799
        // make sure the isImporting flag is transferred, so all hooks know if
5800
        // the current process is an import process
5801
        $copyTCE->isImporting = $this->isImporting;
5802
        $copyTCE->bypassAccessCheckForRecords = $this->bypassAccessCheckForRecords;
5803
        $copyTCE->bypassWorkspaceRestrictions = $this->bypassWorkspaceRestrictions;
5804
        return $copyTCE;
5805
    }
5806
5807
    /**
5808
     * Processes the fields with references as registered during the copy process. This includes all FlexForm fields which had references.
5809
     * @internal should only be used from within DataHandler
5810
     */
5811
    public function remapListedDBRecords()
5812
    {
5813
        if (!empty($this->registerDBList)) {
5814
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5815
            foreach ($this->registerDBList as $table => $records) {
5816
                foreach ($records as $uid => $fields) {
5817
                    $newData = [];
5818
                    $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5819
                    $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5820
                    foreach ($fields as $fieldName => $value) {
5821
                        $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5822
                        switch ($conf['type']) {
5823
                            case 'group':
5824
                            case 'select':
5825
                                $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5826
                                if (is_array($vArray)) {
5827
                                    $newData[$fieldName] = implode(',', $vArray);
5828
                                }
5829
                                break;
5830
                            case 'flex':
5831
                                if ($value === 'FlexForm_reference') {
5832
                                    // This will fetch the new row for the element
5833
                                    $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5834
                                    if (is_array($origRecordRow)) {
5835
                                        BackendUtility::workspaceOL($table, $origRecordRow);
5836
                                        // Get current data structure and value array:
5837
                                        $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5838
                                            ['config' => $conf],
5839
                                            $table,
5840
                                            $fieldName,
5841
                                            $origRecordRow
5842
                                        );
5843
                                        $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5844
                                        $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5845
                                        // Do recursive processing of the XML data:
5846
                                        $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5847
                                        // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5848
                                        if (is_array($currentValueArray['data'])) {
5849
                                            $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5850
                                        }
5851
                                    }
5852
                                }
5853
                                break;
5854
                            case 'inline':
5855
                                $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5856
                                break;
5857
                            default:
5858
                                $this->logger->debug('Field type should not appear here: ' . $conf['type']);
5859
                        }
5860
                    }
5861
                    // If any fields were changed, those fields are updated!
5862
                    if (!empty($newData)) {
5863
                        $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5864
                    }
5865
                }
5866
            }
5867
        }
5868
    }
5869
5870
    /**
5871
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
5872
     *
5873
     * @param array $pParams Set of parameters in numeric array: table, uid, field
5874
     * @param array $dsConf TCA config for field (from Data Structure of course)
5875
     * @param string $dataValue Field value (from FlexForm XML)
5876
     * @param string $dataValue_ext1 Not used
5877
     * @param string $dataValue_ext2 Not used
5878
     * @return array Array where the "value" key carries the value.
5879
     * @see checkValue_flex_procInData_travDS()
5880
     * @see remapListedDBRecords()
5881
     * @internal should only be used from within DataHandler
5882
     */
5883
    public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
5884
    {
5885
        // Extract parameters:
5886
        [$table, $uid, $field] = $pParams;
5887
        // If references are set for this field, set flag so they can be corrected later:
5888
        if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5889
            $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5890
            if (is_array($vArray)) {
5891
                $dataValue = implode(',', $vArray);
5892
            }
5893
        }
5894
        // Return
5895
        return ['value' => $dataValue];
5896
    }
5897
5898
    /**
5899
     * Performs remapping of old UID values to NEW uid values for a DB reference field.
5900
     *
5901
     * @param array $conf TCA field config
5902
     * @param string $value Field value
5903
     * @param int $MM_localUid UID of local record (for MM relations - might need to change if support for FlexForms should be done!)
5904
     * @param string $table Table name
5905
     * @return array|null Returns array of items ready to implode for field content.
5906
     * @see remapListedDBRecords()
5907
     * @internal should only be used from within DataHandler
5908
     */
5909
    public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
5910
    {
5911
        // Initialize variables
5912
        // Will be set TRUE if an upgrade should be done...
5913
        $set = false;
5914
        // Allowed tables for references.
5915
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5916
        // Table name to prepend the UID
5917
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5918
        // Which tables that should possibly not be remapped
5919
        $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5920
        // Convert value to list of references:
5921
        $dbAnalysis = $this->createRelationHandlerInstance();
5922
        $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && $conf['allowNonIdValues'];
5923
        $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5924
        // Traverse those references and map IDs:
5925
        foreach ($dbAnalysis->itemArray as $k => $v) {
5926
            $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5927
            if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5928
                $dbAnalysis->itemArray[$k]['id'] = $mapID;
5929
                $set = true;
5930
            }
5931
        }
5932
        if (!empty($conf['MM'])) {
5933
            // Purge invalid items (live/version)
5934
            $dbAnalysis->purgeItemArray();
5935
            if ($dbAnalysis->isPurged()) {
5936
                $set = true;
5937
            }
5938
5939
            // If record has been versioned/copied in this process, handle invalid relations of the live record
5940
            $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5941
            $originalId = 0;
5942
            if (!empty($this->copyMappingArray_merged[$table])) {
5943
                $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5944
            }
5945
            if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5946
                $liveRelations = $this->createRelationHandlerInstance();
5947
                $liveRelations->setWorkspaceId(0);
5948
                $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5949
                // Purge invalid relations in the live workspace ("0")
5950
                $liveRelations->purgeItemArray(0);
5951
                if ($liveRelations->isPurged()) {
5952
                    $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

5952
                    $liveRelations->writeMM($conf['MM'], $liveId, /** @scrutinizer ignore-type */ $prependName);
Loading history...
5953
                }
5954
            }
5955
        }
5956
        // If a change has been done, set the new value(s)
5957
        if ($set) {
5958
            if ($conf['MM']) {
5959
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5960
            } else {
5961
                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

5961
                return $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName);
Loading history...
5962
            }
5963
        }
5964
        return null;
5965
    }
5966
5967
    /**
5968
     * Performs remapping of old UID values to NEW uid values for an inline field.
5969
     *
5970
     * @param array $conf TCA field config
5971
     * @param string $value Field value
5972
     * @param int $uid The uid of the ORIGINAL record
5973
     * @param string $table Table name
5974
     * @internal should only be used from within DataHandler
5975
     */
5976
    public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
5977
    {
5978
        $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5979
        if ($conf['foreign_table']) {
5980
            $inlineType = $this->getInlineFieldType($conf);
5981
            if ($inlineType === 'mm') {
5982
                $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5983
            } elseif ($inlineType !== false) {
5984
                /** @var RelationHandler $dbAnalysis */
5985
                $dbAnalysis = $this->createRelationHandlerInstance();
5986
                $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5987
5988
                $updatePidForRecords = [];
5989
                // Update values for specific versioned records
5990
                foreach ($dbAnalysis->itemArray as &$item) {
5991
                    $updatePidForRecords[$item['table']][] = $item['id'];
5992
                    $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5993
                    if ($versionedId !== null) {
5994
                        $updatePidForRecords[$item['table']][] = $versionedId;
5995
                        $item['id'] = $versionedId;
5996
                    }
5997
                }
5998
5999
                // Update child records if using pointer fields ('foreign_field'):
6000
                if ($inlineType === 'field') {
6001
                    $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
6002
                }
6003
                $thePidToUpdate = null;
6004
                // If the current field is set on a page record, update the pid of related child records:
6005
                if ($table === 'pages') {
6006
                    $thePidToUpdate = $theUidToUpdate;
6007
                } elseif (isset($this->registerDBPids[$table][$uid])) {
6008
                    $thePidToUpdate = $this->registerDBPids[$table][$uid];
6009
                    $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
6010
                }
6011
6012
                // Update child records if change to pid is required
6013
                if ($thePidToUpdate && !empty($updatePidForRecords)) {
6014
                    // Ensure that only the default language page is used as PID
6015
                    $thePidToUpdate = $this->getDefaultLanguagePageId($thePidToUpdate);
6016
                    // @todo: this can probably go away
6017
                    // ensure, only live page ids are used as 'pid' values
6018
                    $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
6019
                    if ($liveId !== null) {
6020
                        $thePidToUpdate = $liveId;
6021
                    }
6022
                    $updateValues = ['pid' => $thePidToUpdate];
6023
                    foreach ($updatePidForRecords as $tableName => $uids) {
6024
                        if (empty($tableName) || empty($uids)) {
6025
                            continue;
6026
                        }
6027
                        $conn = GeneralUtility::makeInstance(ConnectionPool::class)
6028
                            ->getConnectionForTable($tableName);
6029
                        foreach ($uids as $updateUid) {
6030
                            $conn->update($tableName, $updateValues, ['uid' => $updateUid]);
6031
                        }
6032
                    }
6033
                }
6034
            }
6035
        }
6036
    }
6037
6038
    /**
6039
     * Processes the $this->remapStack at the end of copying, inserting, etc. actions.
6040
     * The remapStack takes care about the correct mapping of new and old uids in case of relational data.
6041
     * @internal should only be used from within DataHandler
6042
     */
6043
    public function processRemapStack()
6044
    {
6045
        // Processes the remap stack:
6046
        if (is_array($this->remapStack)) {
0 ignored issues
show
introduced by
The condition is_array($this->remapStack) is always true.
Loading history...
6047
            $remapFlexForms = [];
6048
            $hookPayload = [];
6049
6050
            $newValue = null;
6051
            foreach ($this->remapStack as $remapAction) {
6052
                // If no position index for the arguments was set, skip this remap action:
6053
                if (!is_array($remapAction['pos'])) {
6054
                    continue;
6055
                }
6056
                // Load values from the argument array in remapAction:
6057
                $field = $remapAction['field'];
6058
                $id = $remapAction['args'][$remapAction['pos']['id']];
6059
                $rawId = $id;
6060
                $table = $remapAction['args'][$remapAction['pos']['table']];
6061
                $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
6062
                $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
6063
                $additionalData = $remapAction['additionalData'];
6064
                // The record is new and has one or more new ids (in case of versioning/workspaces):
6065
                if (strpos($id, 'NEW') !== false) {
6066
                    // Replace NEW...-ID with real uid:
6067
                    $id = $this->substNEWwithIDs[$id];
6068
                    // If the new parent record is on a non-live workspace or versionized, it has another new id:
6069
                    if (isset($this->autoVersionIdMap[$table][$id])) {
6070
                        $id = $this->autoVersionIdMap[$table][$id];
6071
                    }
6072
                    $remapAction['args'][$remapAction['pos']['id']] = $id;
6073
                }
6074
                // Replace relations to NEW...-IDs in field value (uids of child records):
6075
                if (is_array($valueArray)) {
6076
                    foreach ($valueArray as $key => $value) {
6077
                        if (strpos($value, 'NEW') !== false) {
6078
                            if (strpos($value, '_') === false) {
6079
                                $affectedTable = $tcaFieldConf['foreign_table'];
6080
                                $prependTable = false;
6081
                            } else {
6082
                                $parts = explode('_', $value);
6083
                                $value = array_pop($parts);
6084
                                $affectedTable = implode('_', $parts);
6085
                                $prependTable = true;
6086
                            }
6087
                            $value = $this->substNEWwithIDs[$value];
6088
                            // The record is new, but was also auto-versionized and has another new id:
6089
                            if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
6090
                                $value = $this->autoVersionIdMap[$affectedTable][$value];
6091
                            }
6092
                            if ($prependTable) {
6093
                                $value = $affectedTable . '_' . $value;
6094
                            }
6095
                            // Set a hint that this was a new child record:
6096
                            $this->newRelatedIDs[$affectedTable][] = $value;
6097
                            $valueArray[$key] = $value;
6098
                        }
6099
                    }
6100
                    $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
6101
                }
6102
                // Process the arguments with the defined function:
6103
                if (!empty($remapAction['func'])) {
6104
                    $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
6105
                }
6106
                // If array is returned, check for maxitems condition, if string is returned this was already done:
6107
                if (is_array($newValue)) {
6108
                    $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
6109
                    // The reference casting is only required if
6110
                    // checkValue_group_select_processDBdata() returns an array
6111
                    $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
6112
                }
6113
                // Update in database (list of children (csv) or number of relations (foreign_field)):
6114
                if (!empty($field)) {
6115
                    $fieldArray = [$field => $newValue];
6116
                    if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
6117
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
6118
                    }
6119
                    $this->updateDB($table, $id, $fieldArray);
6120
                } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
6121
                    // Collect data to update FlexForms
6122
                    $flexFormId = $additionalData['flexFormId'];
6123
                    $flexFormPath = $additionalData['flexFormPath'];
6124
6125
                    if (!isset($remapFlexForms[$flexFormId])) {
6126
                        $remapFlexForms[$flexFormId] = [];
6127
                    }
6128
6129
                    $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
6130
                }
6131
6132
                // Collect elements that shall trigger processDatamap_afterDatabaseOperations
6133
                if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
6134
                    $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
6135
                    if (!isset($hookPayload[$table][$rawId])) {
6136
                        $hookPayload[$table][$rawId] = [
6137
                            'status' => $hookArgs['status'],
6138
                            'fieldArray' => $hookArgs['fieldArray'],
6139
                            'hookObjects' => $hookArgs['hookObjectsArr'],
6140
                        ];
6141
                    }
6142
                    $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
6143
                }
6144
            }
6145
6146
            if ($remapFlexForms) {
6147
                foreach ($remapFlexForms as $flexFormId => $modifications) {
6148
                    $this->updateFlexFormData($flexFormId, $modifications);
6149
                }
6150
            }
6151
6152
            foreach ($hookPayload as $tableName => $rawIdPayload) {
6153
                foreach ($rawIdPayload as $rawId => $payload) {
6154
                    foreach ($payload['hookObjects'] as $hookObject) {
6155
                        if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
6156
                            continue;
6157
                        }
6158
                        $hookObject->processDatamap_afterDatabaseOperations(
6159
                            $payload['status'],
6160
                            $tableName,
6161
                            $rawId,
6162
                            $payload['fieldArray'],
6163
                            $this
6164
                        );
6165
                    }
6166
                }
6167
            }
6168
        }
6169
        // Processes the remap stack actions:
6170
        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...
6171
            foreach ($this->remapStackActions as $action) {
6172
                if (isset($action['callback'], $action['arguments'])) {
6173
                    call_user_func_array($action['callback'], $action['arguments']);
6174
                }
6175
            }
6176
        }
6177
        // Processes the reference index updates of the remap stack:
6178
        foreach ($this->remapStackRefIndex as $table => $idArray) {
6179
            foreach ($idArray as $id) {
6180
                $this->updateRefIndex($table, $id);
6181
                unset($this->remapStackRefIndex[$table][$id]);
6182
            }
6183
        }
6184
        // Reset:
6185
        $this->remapStack = [];
6186
        $this->remapStackRecords = [];
6187
        $this->remapStackActions = [];
6188
        $this->remapStackRefIndex = [];
6189
    }
6190
6191
    /**
6192
     * Updates FlexForm data.
6193
     *
6194
     * @param string $flexFormId e.g. <table>:<uid>:<field>
6195
     * @param array $modifications Modifications with paths and values (e.g. 'sDEF/lDEV/field/vDEF' => 'TYPO3')
6196
     */
6197
    protected function updateFlexFormData($flexFormId, array $modifications)
6198
    {
6199
        [$table, $uid, $field] = explode(':', $flexFormId, 3);
6200
6201
        if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
6202
            $uid = $this->substNEWwithIDs[$uid];
6203
        }
6204
6205
        $record = $this->recordInfo($table, $uid, '*');
6206
6207
        if (!$table || !$uid || !$field || !is_array($record)) {
6208
            return;
6209
        }
6210
6211
        BackendUtility::workspaceOL($table, $record);
6212
6213
        // Get current data structure and value array:
6214
        $valueStructure = GeneralUtility::xml2array($record[$field]);
6215
6216
        // Do recursive processing of the XML data:
6217
        foreach ($modifications as $path => $value) {
6218
            $valueStructure['data'] = ArrayUtility::setValueByPath(
6219
                $valueStructure['data'],
6220
                $path,
6221
                $value
6222
            );
6223
        }
6224
6225
        if (is_array($valueStructure['data'])) {
6226
            // The return value should be compiled back into XML
6227
            $values = [
6228
                $field => $this->checkValue_flexArray2Xml($valueStructure, true),
6229
            ];
6230
6231
            $this->updateDB($table, $uid, $values);
6232
        }
6233
    }
6234
6235
    /**
6236
     * Triggers a remap action for a specific record.
6237
     *
6238
     * Some records are post-processed by the processRemapStack() method (e.g. IRRE children).
6239
     * This method determines whether an action/modification is executed directly to a record
6240
     * or is postponed to happen after remapping data.
6241
     *
6242
     * @param string $table Name of the table
6243
     * @param string $id Id of the record (can also be a "NEW..." string)
6244
     * @param array $callback The method to be called
6245
     * @param array $arguments The arguments to be submitted to the callback method
6246
     * @param bool $forceRemapStackActions Whether to force to use the stack
6247
     * @see processRemapStack
6248
     */
6249
    protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
6250
    {
6251
        // Check whether the affected record is marked to be remapped:
6252
        if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
6253
            call_user_func_array($callback, $arguments);
6254
        } else {
6255
            $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

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

7104
                    $this->getRecordHistoryStore()->addRecord($table, $id, /** @scrutinizer ignore-type */ $newRow, $this->correlationId);
Loading history...
7105
7106
                    if ($newVersion) {
7107
                        if ($this->enableLogging) {
7108
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7109
                            $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);
7110
                        }
7111
                    } else {
7112
                        if ($this->enableLogging) {
7113
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7114
                            $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
7115
                            $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);
7116
                        }
7117
                        // Clear cache for relevant pages:
7118
                        $this->registerRecordIdForPageCacheClearing($table, $id);
7119
                    }
7120
                    return $id;
7121
                }
7122
                if ($this->enableLogging) {
7123
                    $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

7123
                    $this->log($table, /** @scrutinizer ignore-type */ $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
Loading history...
7124
                }
7125
            }
7126
        }
7127
        return null;
7128
    }
7129
7130
    /**
7131
     * Checking stored record to see if the written values are properly updated.
7132
     *
7133
     * @param string $table Record table name
7134
     * @param int $id Record uid
7135
     * @param array $fieldArray Array of field=>value pairs to insert/update
7136
     * @param string $action Action, for logging only.
7137
     * @return array|null Selected row
7138
     * @see insertDB()
7139
     * @see updateDB()
7140
     * @internal should only be used from within DataHandler
7141
     */
7142
    public function checkStoredRecord($table, $id, $fieldArray, $action)
7143
    {
7144
        $id = (int)$id;
7145
        if (is_array($GLOBALS['TCA'][$table]) && $id) {
7146
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7147
            $queryBuilder->getRestrictions()->removeAll();
7148
7149
            $row = $queryBuilder
7150
                ->select('*')
7151
                ->from($table)
7152
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7153
                ->execute()
7154
                ->fetch();
7155
7156
            if (!empty($row)) {
7157
                // Traverse array of values that was inserted into the database and compare with the actually stored value:
7158
                $errors = [];
7159
                foreach ($fieldArray as $key => $value) {
7160
                    if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
7161
                        if (is_float($row[$key])) {
7162
                            // if the database returns the value as double, compare it as double
7163
                            if ((double)$value !== (double)$row[$key]) {
7164
                                $errors[] = $key;
7165
                            }
7166
                        } else {
7167
                            $dbType = $GLOBALS['TCA'][$table]['columns'][$key]['config']['dbType'] ?? false;
7168
                            if ($dbType === 'datetime' || $dbType === 'time') {
7169
                                $row[$key] = $this->normalizeTimeFormat($table, $row[$key], $dbType);
7170
                            }
7171
                            if ((string)$value !== (string)$row[$key]) {
7172
                                // The is_numeric check catches cases where we want to store a float/double value
7173
                                // and database returns the field as a string with the least required amount of
7174
                                // significant digits, i.e. "0.00" being saved and "0" being read back.
7175
                                if (is_numeric($value) && is_numeric($row[$key])) {
7176
                                    if ((double)$value === (double)$row[$key]) {
7177
                                        continue;
7178
                                    }
7179
                                }
7180
                                $errors[] = $key;
7181
                            }
7182
                        }
7183
                    }
7184
                }
7185
                // Set log message if there were fields with unmatching values:
7186
                if (!empty($errors)) {
7187
                    $message = sprintf(
7188
                        '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.',
7189
                        $id,
7190
                        $table,
7191
                        implode(', ', $errors)
7192
                    );
7193
                    $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

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

7785
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
7786
        return [$parentUid];
7787
    }
7788
7789
    /**
7790
     * Extract entries from TSconfig for a specific table. This will merge specific and default configuration together.
7791
     *
7792
     * @param string $table Table name
7793
     * @param array $TSconfig TSconfig for page
7794
     * @return array TSconfig merged
7795
     * @internal should only be used from within DataHandler
7796
     */
7797
    public function getTableEntries($table, $TSconfig)
7798
    {
7799
        $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : [];
7800
        $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : [];
7801
        ArrayUtility::mergeRecursiveWithOverrule($dA, $tA);
7802
        return $dA;
7803
    }
7804
7805
    /**
7806
     * Returns the pid of a record from $table with $uid
7807
     *
7808
     * @param string $table Table name
7809
     * @param int $uid Record uid
7810
     * @return int|false PID value (unless the record did not exist in which case FALSE is returned)
7811
     * @internal should only be used from within DataHandler
7812
     */
7813
    public function getPID($table, $uid)
7814
    {
7815
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7816
        $queryBuilder->getRestrictions()
7817
            ->removeAll();
7818
        $queryBuilder->select('pid')
7819
            ->from($table)
7820
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
7821
        if ($row = $queryBuilder->execute()->fetch()) {
7822
            return $row['pid'];
7823
        }
7824
        return false;
7825
    }
7826
7827
    /**
7828
     * Executing dbAnalysisStore
7829
     * This will save MM relations for new records but is executed after records are created because we need to know the ID of them
7830
     * @internal should only be used from within DataHandler
7831
     */
7832
    public function dbAnalysisStoreExec()
7833
    {
7834
        foreach ($this->dbAnalysisStore as $action) {
7835
            $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
7836
            if ($id) {
7837
                $action[0]->writeMM($action[1], $id, $action[3]);
7838
            }
7839
        }
7840
    }
7841
7842
    /**
7843
     * Returns array, $CPtable, of pages under the $pid going down to $counter levels.
7844
     * Selecting ONLY pages which the user has read-access to!
7845
     *
7846
     * @param array $CPtable Accumulation of page uid=>pid pairs in branch of $pid
7847
     * @param int $pid Page ID for which to find subpages
7848
     * @param int $counter Number of levels to go down.
7849
     * @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!
7850
     * @return array Return array.
7851
     * @internal should only be used from within DataHandler
7852
     */
7853
    public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
7854
    {
7855
        if ($counter) {
7856
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
7857
            $restrictions = $queryBuilder->getRestrictions()->removeAll();
7858
            $this->addDeleteRestriction($restrictions);
7859
            $queryBuilder
7860
                ->select('uid')
7861
                ->from('pages')
7862
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7863
                ->orderBy('sorting', 'DESC');
7864
            if (!$this->admin) {
7865
                $queryBuilder->andWhere($this->BE_USER->getPagePermsClause(Permission::PAGE_SHOW));
7866
            }
7867
            if ((int)$this->BE_USER->workspace === 0) {
7868
                $queryBuilder->andWhere(
7869
                    $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
7870
                );
7871
            } else {
7872
                $queryBuilder->andWhere($queryBuilder->expr()->in(
7873
                    't3ver_wsid',
7874
                    $queryBuilder->createNamedParameter([0, $this->BE_USER->workspace], Connection::PARAM_INT_ARRAY)
7875
                ));
7876
            }
7877
            $result = $queryBuilder->execute();
7878
7879
            $pages = [];
7880
            while ($row = $result->fetch()) {
7881
                $pages[$row['uid']] = $row;
7882
            }
7883
7884
            // Resolve placeholders of workspace versions
7885
            if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
7886
                $pages = array_reverse(
7887
                    $this->resolveVersionedRecords(
7888
                        'pages',
7889
                        'uid',
7890
                        'sorting',
7891
                        array_keys($pages)
7892
                    ),
7893
                    true
7894
                );
7895
            }
7896
7897
            foreach ($pages as $page) {
7898
                if ($page['uid'] != $rootID) {
7899
                    $CPtable[$page['uid']] = $pid;
7900
                    // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
7901
                    if ($counter - 1) {
7902
                        $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
7903
                    }
7904
                }
7905
            }
7906
        }
7907
        return $CPtable;
7908
    }
7909
7910
    /**
7911
     * List of all tables (those administrators has access to = array_keys of $GLOBALS['TCA'])
7912
     *
7913
     * @return array Array of all TCA table names
7914
     * @internal should only be used from within DataHandler
7915
     */
7916
    public function compileAdminTables()
7917
    {
7918
        return array_keys($GLOBALS['TCA']);
7919
    }
7920
7921
    /**
7922
     * Checks if any uniqueInPid eval input fields are in the record and if so, they are re-written to be correct.
7923
     *
7924
     * @param string $table Table name
7925
     * @param int $uid Record UID
7926
     * @internal should only be used from within DataHandler
7927
     */
7928
    public function fixUniqueInPid($table, $uid)
7929
    {
7930
        if (empty($GLOBALS['TCA'][$table])) {
7931
            return;
7932
        }
7933
7934
        $curData = $this->recordInfo($table, $uid, '*');
7935
        $newData = [];
7936
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7937
            if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
7938
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7939
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
7940
                    $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
7941
                    if ((string)$newV !== (string)$curData[$field]) {
7942
                        $newData[$field] = $newV;
7943
                    }
7944
                }
7945
            }
7946
        }
7947
        // IF there are changed fields, then update the database
7948
        if (!empty($newData)) {
7949
            $this->updateDB($table, $uid, $newData);
7950
        }
7951
    }
7952
7953
    /**
7954
     * Checks if any uniqueInSite eval fields are in the record and if so, they are re-written to be correct.
7955
     *
7956
     * @param string $table Table name
7957
     * @param int $uid Record UID
7958
     * @return bool whether the record had to be fixed or not
7959
     */
7960
    protected function fixUniqueInSite(string $table, int $uid): bool
7961
    {
7962
        $curData = $this->recordInfo($table, $uid, '*');
7963
        $workspaceId = $this->BE_USER->workspace;
7964
        $newData = [];
7965
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7966
            if ($conf['config']['type'] === 'slug' && (string)$curData[$field] !== '') {
7967
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7968
                if (in_array('uniqueInSite', $evalCodesArray, true)) {
7969
                    $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $conf['config'], $workspaceId);
7970
                    $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

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

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