Passed
Push — master ( 5f3463...5e7cad )
by
unknown
14:58
created

DataHandler::addRemapAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

1211
            $record = $this->recordInfo($table, /** @scrutinizer ignore-type */ abs($pid), 'pid');
Loading history...
1212
            // Ensure that the "pid" is not a translated page ID, but the default page ID
1213
            $fieldArray['pid'] = $this->getDefaultLanguagePageId($record['pid']);
1214
        }
1215
        return $fieldArray;
1216
    }
1217
1218
    /**
1219
     * Filling in the field array
1220
     * $this->excludedTablesAndFields is used to filter fields if needed.
1221
     *
1222
     * @param string $table Table name
1223
     * @param int $id Record ID
1224
     * @param array $fieldArray Default values, Preset $fieldArray with 'pid' maybe (pid and uid will be not be overridden anyway)
1225
     * @param array $incomingFieldArray Is which fields/values you want to set. There are processed and put into $fieldArray if OK
1226
     * @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.
1227
     * @param string $status Is 'new' or 'update'
1228
     * @param int $tscPID TSconfig PID
1229
     * @return array Field Array
1230
     * @internal should only be used from within DataHandler
1231
     */
1232
    public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID)
1233
    {
1234
        // Initialize:
1235
        $originalLanguageRecord = null;
1236
        $originalLanguage_diffStorage = null;
1237
        $diffStorageFlag = false;
1238
        // Setting 'currentRecord' and 'checkValueRecord':
1239
        if (strpos($id, 'NEW') !== false) {
1240
            // Must have the 'current' array - not the values after processing below...
1241
            $checkValueRecord = $fieldArray;
1242
            // IF $incomingFieldArray is an array, overlay it.
1243
            // 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...
1244
            if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {
0 ignored issues
show
introduced by
The condition is_array($checkValueRecord) is always true.
Loading history...
1245
                ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);
1246
            }
1247
            $currentRecord = $checkValueRecord;
1248
        } else {
1249
            // We must use the current values as basis for this!
1250
            $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));
1251
        }
1252
1253
        // Get original language record if available:
1254
        if (is_array($currentRecord)
1255
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']
1256
            && $GLOBALS['TCA'][$table]['ctrl']['languageField']
1257
            && $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
1258
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
1259
            && (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0
1260
        ) {
1261
            $originalLanguageRecord = $this->recordInfo($table, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');
1262
            BackendUtility::workspaceOL($table, $originalLanguageRecord);
1263
            $originalLanguage_diffStorage = json_decode(
1264
                (string)($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] ?? ''),
1265
                true
1266
            );
1267
        }
1268
1269
        $this->checkValue_currentRecord = $checkValueRecord;
1270
        // In the following all incoming value-fields are tested:
1271
        // - Are the user allowed to change the field?
1272
        // - Is the field uid/pid (which are already set)
1273
        // - perms-fields for pages-table, then do special things...
1274
        // - If the field is nothing of the above and the field is configured in TCA, the fieldvalues are evaluated by ->checkValue
1275
        // If everything is OK, the field is entered into $fieldArray[]
1276
        foreach ($incomingFieldArray as $field => $fieldValue) {
1277
            if (isset($this->excludedTablesAndFields[$table . '-' . $field]) || $this->data_disableFields[$table][$id][$field]) {
1278
                continue;
1279
            }
1280
1281
            // The field must be editable.
1282
            // Checking if a value for language can be changed:
1283
            $languageDeny = $GLOBALS['TCA'][$table]['ctrl']['languageField'] && (string)$GLOBALS['TCA'][$table]['ctrl']['languageField'] === (string)$field && !$this->BE_USER->checkLanguageAccess($fieldValue);
1284
            if ($languageDeny) {
1285
                continue;
1286
            }
1287
1288
            switch ($field) {
1289
                case 'uid':
1290
                case 'pid':
1291
                    // Nothing happens, already set
1292
                    break;
1293
                case 'perms_userid':
1294
                case 'perms_groupid':
1295
                case 'perms_user':
1296
                case 'perms_group':
1297
                case 'perms_everybody':
1298
                    // Permissions can be edited by the owner or the administrator
1299
                    if ($table === 'pages' && ($this->admin || $status === 'new' || $this->pageInfo($id, 'perms_userid') == $this->userid)) {
1300
                        $value = (int)$fieldValue;
1301
                        switch ($field) {
1302
                            case 'perms_userid':
1303
                            case 'perms_groupid':
1304
                                $fieldArray[$field] = $value;
1305
                                break;
1306
                            default:
1307
                                if ($value >= 0 && $value < (2 ** 5)) {
1308
                                    $fieldArray[$field] = $value;
1309
                                }
1310
                        }
1311
                    }
1312
                    break;
1313
                case 't3ver_oid':
1314
                case 't3ver_wsid':
1315
                case 't3ver_state':
1316
                case 't3ver_stage':
1317
                    break;
1318
                case 'l10n_state':
1319
                    $fieldArray[$field] = $fieldValue;
1320
                    break;
1321
                default:
1322
                    if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
1323
                        // Evaluating the value
1324
                        $res = $this->checkValue($table, $field, $fieldValue, $id, $status, $realPid, $tscPID, $incomingFieldArray);
1325
                        if (array_key_exists('value', $res)) {
1326
                            $fieldArray[$field] = $res['value'];
1327
                        }
1328
                        // Add the value of the original record to the diff-storage content:
1329
                        if ($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
1330
                            $originalLanguage_diffStorage[$field] = (string)$originalLanguageRecord[$field];
1331
                            $diffStorageFlag = true;
1332
                        }
1333
                    } elseif ($GLOBALS['TCA'][$table]['ctrl']['origUid'] === $field) {
1334
                        // Allow value for original UID to pass by...
1335
                        $fieldArray[$field] = $fieldValue;
1336
                    }
1337
            }
1338
        }
1339
1340
        // Dealing with a page translation, setting "sorting", "pid", "perms_*" to the same values as the original record
1341
        if ($table === 'pages' && is_array($originalLanguageRecord)) {
1342
            $fieldArray['sorting'] = $originalLanguageRecord['sorting'];
1343
            $fieldArray['perms_userid'] = $originalLanguageRecord['perms_userid'];
1344
            $fieldArray['perms_groupid'] = $originalLanguageRecord['perms_groupid'];
1345
            $fieldArray['perms_user'] = $originalLanguageRecord['perms_user'];
1346
            $fieldArray['perms_group'] = $originalLanguageRecord['perms_group'];
1347
            $fieldArray['perms_everybody'] = $originalLanguageRecord['perms_everybody'];
1348
        }
1349
1350
        // Add diff-storage information:
1351
        if ($diffStorageFlag
1352
            && !array_key_exists($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
1353
        ) {
1354
            // 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...
1355
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = json_encode($originalLanguage_diffStorage);
1356
        }
1357
        // Return fieldArray
1358
        return $fieldArray;
1359
    }
1360
1361
    /*********************************************
1362
     *
1363
     * Evaluation of input values
1364
     *
1365
     ********************************************/
1366
    /**
1367
     * Evaluates a value according to $table/$field settings.
1368
     * This function is for real database fields - NOT FlexForm "pseudo" fields.
1369
     * NOTICE: Calling this function expects this: 1) That the data is saved!
1370
     *
1371
     * @param string $table Table name
1372
     * @param string $field Field name
1373
     * @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.
1374
     * @param string $id The record-uid, mainly - but not exclusively - used for logging
1375
     * @param string $status 'update' or 'new' flag
1376
     * @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.
1377
     * @param int $tscPID TSconfig PID
1378
     * @param array $incomingFieldArray the fields being explicitly set by the outside (unlike $fieldArray)
1379
     * @return array Returns the evaluated $value as key "value" in this array. Can be checked with isset($res['value']) ...
1380
     * @internal should only be used from within DataHandler
1381
     */
1382
    public function checkValue($table, $field, $value, $id, $status, $realPid, $tscPID, $incomingFieldArray = [])
1383
    {
1384
        $curValueRec = null;
1385
        // Result array
1386
        $res = [];
1387
1388
        // Processing special case of field pages.doktype
1389
        if ($table === 'pages' && $field === 'doktype') {
1390
            // If the user may not use this specific doktype, we issue a warning
1391
            if (!($this->admin || GeneralUtility::inList($this->BE_USER->groupData['pagetypes_select'], $value))) {
1392
                if ($this->enableLogging) {
1393
                    $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

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

1394
                    $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...
1395
                }
1396
                return $res;
1397
            }
1398
            if ($status === 'update') {
1399
                // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1400
                $onlyAllowedTables = $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1401
                if ($onlyAllowedTables) {
1402
                    // use the real page id (default language)
1403
                    $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

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

1404
                    $theWrongTables = $this->doesPageHaveUnallowedTables($recordId, /** @scrutinizer ignore-type */ $value);
Loading history...
1405
                    if ($theWrongTables) {
1406
                        if ($this->enableLogging) {
1407
                            $propArr = $this->getRecordProperties($table, $id);
1408
                            $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']);
1409
                        }
1410
                        return $res;
1411
                    }
1412
                }
1413
            }
1414
        }
1415
1416
        $curValue = null;
1417
        if ((int)$id !== 0) {
1418
            // Get current value:
1419
            $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

1419
            $curValueRec = $this->recordInfo($table, /** @scrutinizer ignore-type */ $id, $field);
Loading history...
1420
            // isset() won't work here, since values can be NULL
1421
            if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1422
                $curValue = $curValueRec[$field];
1423
            }
1424
        }
1425
1426
        if ($table === 'be_users'
1427
            && ($field === 'admin' || $field === 'password')
1428
            && $status === 'update'
1429
        ) {
1430
            // Do not allow a non system maintainer admin to change admin flag and password of system maintainers
1431
            $systemMaintainers = array_map('intval', $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
1432
            // False if current user is not in system maintainer list or if switch to user mode is active
1433
            $isCurrentUserSystemMaintainer = $this->BE_USER->isSystemMaintainer();
1434
            $isTargetUserInSystemMaintainerList = in_array((int)$id, $systemMaintainers, true);
1435
            if ($field === 'admin') {
1436
                $isFieldChanged = (int)$curValueRec[$field] !== (int)$value;
1437
            } else {
1438
                $isFieldChanged = $curValueRec[$field] !== $value;
1439
            }
1440
            if (!$isCurrentUserSystemMaintainer && $isTargetUserInSystemMaintainerList && $isFieldChanged) {
1441
                $value = $curValueRec[$field];
1442
                $message = GeneralUtility::makeInstance(
1443
                    FlashMessage::class,
1444
                    $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.adminCanNotChangeSystemMaintainer'),
1445
                    '',
1446
                    FlashMessage::ERROR,
1447
                    true
1448
                );
1449
                $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1450
                $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
1451
            }
1452
        }
1453
1454
        // Getting config for the field
1455
        $tcaFieldConf = $this->resolveFieldConfigurationAndRespectColumnsOverrides($table, $field);
1456
1457
        // Create $recFID only for those types that need it
1458
        if ($tcaFieldConf['type'] === 'flex') {
1459
            $recFID = $table . ':' . $id . ':' . $field;
1460
        } else {
1461
            $recFID = null;
1462
        }
1463
1464
        // Perform processing:
1465
        $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

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

2520
                    $value = number_format(/** @scrutinizer ignore-type */ $value, 2, '.', '');
Loading history...
2521
                    break;
2522
                case 'md5':
2523
                    if (strlen($value) !== 32) {
2524
                        $set = false;
2525
                    }
2526
                    break;
2527
                case 'trim':
2528
                    $value = trim($value);
2529
                    break;
2530
                case 'upper':
2531
                    $value = mb_strtoupper($value, 'utf-8');
2532
                    break;
2533
                case 'lower':
2534
                    $value = mb_strtolower($value, 'utf-8');
2535
                    break;
2536
                case 'required':
2537
                    if (!isset($value) || $value === '') {
2538
                        $set = false;
2539
                    }
2540
                    break;
2541
                case 'is_in':
2542
                    $c = mb_strlen($value);
2543
                    if ($c) {
2544
                        $newVal = '';
2545
                        for ($a = 0; $a < $c; $a++) {
2546
                            $char = mb_substr($value, $a, 1);
2547
                            if (mb_strpos($is_in, $char) !== false) {
2548
                                $newVal .= $char;
2549
                            }
2550
                        }
2551
                        $value = $newVal;
2552
                    }
2553
                    break;
2554
                case 'nospace':
2555
                    $value = str_replace(' ', '', $value);
2556
                    break;
2557
                case 'alpha':
2558
                    $value = preg_replace('/[^a-zA-Z]/', '', $value);
2559
                    break;
2560
                case 'num':
2561
                    $value = preg_replace('/[^0-9]/', '', $value);
2562
                    break;
2563
                case 'alphanum':
2564
                    $value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
2565
                    break;
2566
                case 'alphanum_x':
2567
                    $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
2568
                    break;
2569
                case 'domainname':
2570
                    if (!preg_match('/^[a-z0-9.\\-]*$/i', $value)) {
2571
                        $value = (string)HttpUtility::idn_to_ascii($value);
2572
                    }
2573
                    break;
2574
                case 'email':
2575
                    if ((string)$value !== '') {
2576
                        $this->checkValue_input_ValidateEmail($value, $set);
2577
                    }
2578
                    break;
2579
                case 'saltedPassword':
2580
                    // An incoming value is either the salted password if the user did not change existing password
2581
                    // when submitting the form, or a plaintext new password that needs to be turned into a salted password now.
2582
                    // The strategy is to see if a salt instance can be created from the incoming value. If so,
2583
                    // no new password was submitted and we keep the value. If no salting instance can be created,
2584
                    // incoming value must be a new plain text value that needs to be hashed.
2585
                    $hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
2586
                    $mode = $table === 'fe_users' ? 'FE' : 'BE';
2587
                    try {
2588
                        $hashFactory->get($value, $mode);
2589
                    } catch (InvalidPasswordHashException $e) {
2590
                        // We got no salted password instance, incoming value must be a new plaintext password
2591
                        // Get an instance of the current configured salted password strategy and hash the value
2592
                        $newHashInstance = $hashFactory->getDefaultHashInstance($mode);
2593
                        $value = $newHashInstance->getHashedPassword($value);
2594
                    }
2595
                    break;
2596
                default:
2597
                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2598
                        if (class_exists($func)) {
2599
                            $evalObj = GeneralUtility::makeInstance($func);
2600
                            if (method_exists($evalObj, 'evaluateFieldValue')) {
2601
                                $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2602
                            }
2603
                        }
2604
                    }
2605
            }
2606
        }
2607
        if ($set) {
2608
            $res['value'] = $value;
2609
        }
2610
        return $res;
2611
    }
2612
2613
    /**
2614
     * If $value is not a valid e-mail address,
2615
     * $set will be set to false and a flash error
2616
     * message will be added
2617
     *
2618
     * @param string $value Value to evaluate
2619
     * @param bool $set TRUE if an update should be done
2620
     * @throws \InvalidArgumentException
2621
     * @throws \TYPO3\CMS\Core\Exception
2622
     */
2623
    protected function checkValue_input_ValidateEmail($value, &$set)
2624
    {
2625
        if (GeneralUtility::validEmail($value)) {
2626
            return;
2627
        }
2628
2629
        $set = false;
2630
        /** @var FlashMessage $message */
2631
        $message = GeneralUtility::makeInstance(
2632
            FlashMessage::class,
2633
            sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.invalidEmail'), $value),
2634
            '', // header is optional
2635
            FlashMessage::ERROR,
2636
            true // whether message should be stored in session
2637
        );
2638
        /** @var FlashMessageService $flashMessageService */
2639
        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
2640
        $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
2641
    }
2642
2643
    /**
2644
     * Returns data for group/db and select fields
2645
     *
2646
     * @param array $valueArray Current value array
2647
     * @param array $tcaFieldConf TCA field config
2648
     * @param int $id Record id, used for look-up of MM relations (local_uid)
2649
     * @param string $status Status string ('update' or 'new')
2650
     * @param string $type The type, either 'select', 'group' or 'inline'
2651
     * @param string $currentTable Table name, needs to be passed to \TYPO3\CMS\Core\Database\RelationHandler
2652
     * @param string $currentField field name, needs to be set for writing to sys_history
2653
     * @return array Modified value array
2654
     * @internal should only be used from within DataHandler
2655
     */
2656
    public function checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
2657
    {
2658
        if ($type === 'group') {
2659
            $tables = $tcaFieldConf['allowed'];
2660
        } elseif (!empty($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages') {
2661
            $tables = 'sys_language';
2662
        } else {
2663
            $tables = $tcaFieldConf['foreign_table'];
2664
        }
2665
        $prep = $type === 'group' ? $tcaFieldConf['prepend_tname'] : '';
2666
        $newRelations = implode(',', $valueArray);
2667
        /** @var RelationHandler $dbAnalysis */
2668
        $dbAnalysis = $this->createRelationHandlerInstance();
2669
        $dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2670
        $dbAnalysis->start($newRelations, $tables, '', 0, $currentTable, $tcaFieldConf);
2671
        if ($tcaFieldConf['MM']) {
2672
            // convert submitted items to use version ids instead of live ids
2673
            // (only required for MM relations in a workspace context)
2674
            $dbAnalysis->convertItemArray();
2675
            if ($status === 'update') {
2676
                /** @var RelationHandler $oldRelations_dbAnalysis */
2677
                $oldRelations_dbAnalysis = $this->createRelationHandlerInstance();
2678
                $oldRelations_dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2679
                // Db analysis with $id will initialize with the existing relations
2680
                $oldRelations_dbAnalysis->start('', $tables, $tcaFieldConf['MM'], $id, $currentTable, $tcaFieldConf);
2681
                $oldRelations = implode(',', $oldRelations_dbAnalysis->getValueArray());
2682
                $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

2682
                $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, /** @scrutinizer ignore-type */ $prep);
Loading history...
2683
                if ($oldRelations != $newRelations) {
2684
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
2685
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
2686
                } else {
2687
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
2688
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
2689
                }
2690
            } else {
2691
                $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
2692
            }
2693
            $valueArray = $dbAnalysis->countItems();
2694
        } else {
2695
            $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

2695
            $valueArray = $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prep);
Loading history...
2696
        }
2697
        // 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.
2698
        return $valueArray;
2699
    }
2700
2701
    /**
2702
     * Explodes the $value, which is a list of files/uids (group select)
2703
     *
2704
     * @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.
2705
     * @return array The value array.
2706
     * @internal should only be used from within DataHandler
2707
     */
2708
    public function checkValue_group_select_explodeSelectGroupValue($value)
2709
    {
2710
        $valueArray = GeneralUtility::trimExplode(',', $value, true);
2711
        foreach ($valueArray as &$newVal) {
2712
            $temp = explode('|', $newVal, 2);
2713
            $newVal = str_replace(['|', ','], '', rawurldecode($temp[0]));
2714
        }
2715
        unset($newVal);
2716
        return $valueArray;
2717
    }
2718
2719
    /**
2720
     * Starts the processing the input data for flexforms. This will traverse all sheets / languages and for each it will traverse the sub-structure.
2721
     * See checkValue_flex_procInData_travDS() for more details.
2722
     * 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
2723
     *
2724
     * @param array $dataPart The 'data' part of the INPUT flexform data
2725
     * @param array $dataPart_current The 'data' part of the CURRENT flexform data
2726
     * @param array $uploadedFiles The uploaded files for the 'data' part of the INPUT flexform data
2727
     * @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.
2728
     * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions
2729
     * @param string $callBackFunc Optional call back function, see checkValue_flex_procInData_travDS()  DEPRECATED, use \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools instead for traversal!
2730
     * @param array $workspaceOptions
2731
     * @return array The modified 'data' part.
2732
     * @see checkValue_flex_procInData_travDS()
2733
     * @internal should only be used from within DataHandler
2734
     */
2735
    public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructure, $pParams, $callBackFunc = '', array $workspaceOptions = [])
2736
    {
2737
        if (is_array($dataPart)) {
0 ignored issues
show
introduced by
The condition is_array($dataPart) is always true.
Loading history...
2738
            foreach ($dataPart as $sKey => $sheetDef) {
2739
                if (isset($dataStructure['sheets'][$sKey]) && is_array($dataStructure['sheets'][$sKey]) && is_array($sheetDef)) {
2740
                    foreach ($sheetDef as $lKey => $lData) {
2741
                        $this->checkValue_flex_procInData_travDS(
2742
                            $dataPart[$sKey][$lKey],
2743
                            $dataPart_current[$sKey][$lKey],
2744
                            $uploadedFiles[$sKey][$lKey],
2745
                            $dataStructure['sheets'][$sKey]['ROOT']['el'],
2746
                            $pParams,
2747
                            $callBackFunc,
2748
                            $sKey . '/' . $lKey . '/',
2749
                            $workspaceOptions
2750
                        );
2751
                    }
2752
                }
2753
            }
2754
        }
2755
        return $dataPart;
2756
    }
2757
2758
    /**
2759
     * Processing of the sheet/language data array
2760
     * 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.
2761
     *
2762
     * @param array $dataValues New values (those being processed): Multidimensional Data array for sheet/language, passed by reference!
2763
     * @param array $dataValues_current Current values: Multidimensional Data array. May be empty array() if not needed (for callBackFunctions)
2764
     * @param array $uploadedFiles Uploaded files array for sheet/language. May be empty array() if not needed (for callBackFunctions)
2765
     * @param array $DSelements Data structure which fits the data array
2766
     * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions / call back function
2767
     * @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.
2768
     * @param string $structurePath
2769
     * @param array $workspaceOptions
2770
     * @see checkValue_flex_procInData()
2771
     * @internal should only be used from within DataHandler
2772
     */
2773
    public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions = [])
2774
    {
2775
        if (!is_array($DSelements)) {
0 ignored issues
show
introduced by
The condition is_array($DSelements) is always true.
Loading history...
2776
            return;
2777
        }
2778
2779
        // For each DS element:
2780
        foreach ($DSelements as $key => $dsConf) {
2781
            // Array/Section:
2782
            if ($DSelements[$key]['type'] === 'array') {
2783
                if (!is_array($dataValues[$key]['el'])) {
2784
                    continue;
2785
                }
2786
2787
                if ($DSelements[$key]['section']) {
2788
                    foreach ($dataValues[$key]['el'] as $ik => $el) {
2789
                        if (!is_array($el)) {
2790
                            continue;
2791
                        }
2792
2793
                        if (!is_array($dataValues_current[$key]['el'])) {
2794
                            $dataValues_current[$key]['el'] = [];
2795
                        }
2796
                        $theKey = key($el);
2797
                        if (!is_array($dataValues[$key]['el'][$ik][$theKey]['el'])) {
2798
                            continue;
2799
                        }
2800
2801
                        $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);
2802
                    }
2803
                } else {
2804
                    if (!isset($dataValues[$key]['el'])) {
2805
                        $dataValues[$key]['el'] = [];
2806
                    }
2807
                    $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/', $workspaceOptions);
2808
                }
2809
            } else {
2810
                // When having no specific sheets, it's "TCEforms.config", when having a sheet, it's just "config"
2811
                $fieldConfiguration = $dsConf['TCEforms']['config'] ?? $dsConf['config'] ?? null;
2812
                // init with value from config for passthrough fields
2813
                if (!empty($fieldConfiguration['type']) && $fieldConfiguration['type'] === 'passthrough') {
2814
                    if (!empty($dataValues_current[$key]['vDEF'])) {
2815
                        // If there is existing value, keep it
2816
                        $dataValues[$key]['vDEF'] = $dataValues_current[$key]['vDEF'];
2817
                    } elseif (
2818
                        !empty($fieldConfiguration['default'])
2819
                        && isset($pParams[1])
2820
                        && !MathUtility::canBeInterpretedAsInteger($pParams[1])
2821
                    ) {
2822
                        // If is new record and a default is specified for field, use it.
2823
                        $dataValues[$key]['vDEF'] = $fieldConfiguration['default'];
2824
                    }
2825
                }
2826
                if (!is_array($fieldConfiguration) || !is_array($dataValues[$key])) {
2827
                    continue;
2828
                }
2829
2830
                foreach ($dataValues[$key] as $vKey => $data) {
2831
                    if ($callBackFunc) {
2832
                        if (is_object($this->callBackObj)) {
2833
                            $res = $this->callBackObj->{$callBackFunc}($pParams, $fieldConfiguration, $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
2834
                        } else {
2835
                            $res = $this->{$callBackFunc}($pParams, $fieldConfiguration, $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
2836
                        }
2837
                    } else {
2838
                        // Default
2839
                        [$CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID] = $pParams;
2840
2841
                        $additionalData = [
2842
                            'flexFormId' => $CVrecFID,
2843
                            'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
2844
                        ];
2845
2846
                        $res = $this->checkValue_SW([], $dataValues[$key][$vKey], $fieldConfiguration, $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], $CVtscPID, $additionalData);
2847
                    }
2848
                    // Adding the value:
2849
                    if (isset($res['value'])) {
2850
                        $dataValues[$key][$vKey] = $res['value'];
2851
                    }
2852
                    // 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.
2853
                    // 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).
2854
                    if (mb_substr($vKey, -9) !== '.vDEFbase') {
2855
                        if ($GLOBALS['TYPO3_CONF_VARS']['BE']['flexFormXMLincludeDiffBase'] && $vKey !== 'vDEF' && ((string)$dataValues[$key][$vKey] !== (string)$dataValues_current[$key][$vKey] || !isset($dataValues_current[$key][$vKey . '.vDEFbase']))) {
2856
                            // 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:
2857
                            if (isset($dataValues[$key]['vDEF'])) {
2858
                                $diffValue = $dataValues[$key]['vDEF'];
2859
                            } else {
2860
                                // If not found (for translators with no access to the default language) we use the one from the current-value data set:
2861
                                $diffValue = $dataValues_current[$key]['vDEF'];
2862
                            }
2863
                            // 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.
2864
                            $dataValues[$key][$vKey . '.vDEFbase'] = $diffValue;
2865
                        }
2866
                    }
2867
                }
2868
            }
2869
        }
2870
    }
2871
2872
    /**
2873
     * Returns data for inline fields.
2874
     *
2875
     * @param array $valueArray Current value array
2876
     * @param array $tcaFieldConf TCA field config
2877
     * @param int $id Record id
2878
     * @param string $status Status string ('update' or 'new')
2879
     * @param string $table Table name, needs to be passed to \TYPO3\CMS\Core\Database\RelationHandler
2880
     * @param string $field The current field the values are modified for
2881
     * @param array $additionalData Additional data to be forwarded to sub-processors
2882
     * @return string Modified values
2883
     */
2884
    protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, array $additionalData = null)
2885
    {
2886
        $foreignTable = $tcaFieldConf['foreign_table'];
2887
        $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
2888
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
2889
        /** @var RelationHandler $dbAnalysis */
2890
        $dbAnalysis = $this->createRelationHandlerInstance();
2891
        $dbAnalysis->start(implode(',', $valueArray), $foreignTable, '', 0, $table, $tcaFieldConf);
2892
        // IRRE with a pointer field (database normalization):
2893
        if ($tcaFieldConf['foreign_field']) {
2894
            // if the record was imported, sorting was also imported, so skip this
2895
            $skipSorting = (bool)$this->callFromImpExp;
2896
            // update record in intermediate table (sorting & pointer uid to parent record)
2897
            $dbAnalysis->writeForeignField($tcaFieldConf, $id, 0, $skipSorting);
2898
            $newValue = $dbAnalysis->countItems(false);
2899
        } elseif ($this->getInlineFieldType($tcaFieldConf) === 'mm') {
2900
            // In order to fully support all the MM stuff, directly call checkValue_group_select_processDBdata instead of repeating the needed code here
2901
            $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
2902
            $newValue = $valueArray[0];
2903
        } else {
2904
            $valueArray = $dbAnalysis->getValueArray();
2905
            // Checking that the number of items is correct:
2906
            $valueArray = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
2907
            $newValue = $this->castReferenceValue(implode(',', $valueArray), $tcaFieldConf);
2908
        }
2909
        return $newValue;
2910
    }
2911
2912
    /*********************************************
2913
     *
2914
     * PROCESSING COMMANDS
2915
     *
2916
     ********************************************/
2917
    /**
2918
     * Processing the cmd-array
2919
     * See "TYPO3 Core API" for a description of the options.
2920
     *
2921
     * @return void|bool
2922
     */
2923
    public function process_cmdmap()
2924
    {
2925
        // Editing frozen:
2926
        if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
2927
            $this->newlog('All editing in this workspace has been frozen!', SystemLogErrorClassification::USER_ERROR);
2928
            return false;
2929
        }
2930
        // Hook initialization:
2931
        $hookObjectsArr = [];
2932
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
2933
            $hookObj = GeneralUtility::makeInstance($className);
2934
            if (method_exists($hookObj, 'processCmdmap_beforeStart')) {
2935
                $hookObj->processCmdmap_beforeStart($this);
2936
            }
2937
            $hookObjectsArr[] = $hookObj;
2938
        }
2939
        $pasteDatamap = [];
2940
        // Traverse command map:
2941
        foreach ($this->cmdmap as $table => $_) {
2942
            // Check if the table may be modified!
2943
            $modifyAccessList = $this->checkModifyAccessList($table);
2944
            if (!$modifyAccessList) {
2945
                $this->log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
2946
            }
2947
            // Check basic permissions and circumstances:
2948
            if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->cmdmap[$table]) || !$modifyAccessList) {
2949
                continue;
2950
            }
2951
2952
            // Traverse the command map:
2953
            foreach ($this->cmdmap[$table] as $id => $incomingCmdArray) {
2954
                if (!is_array($incomingCmdArray)) {
2955
                    continue;
2956
                }
2957
2958
                if ($table === 'pages') {
2959
                    // for commands on pages do a pagetree-refresh
2960
                    $this->pagetreeNeedsRefresh = true;
2961
                }
2962
2963
                foreach ($incomingCmdArray as $command => $value) {
2964
                    $pasteUpdate = false;
2965
                    if (is_array($value) && isset($value['action']) && $value['action'] === 'paste') {
2966
                        // Extended paste command: $command is set to "move" or "copy"
2967
                        // $value['update'] holds field/value pairs which should be updated after copy/move operation
2968
                        // $value['target'] holds original $value (target of move/copy)
2969
                        $pasteUpdate = $value['update'];
2970
                        $value = $value['target'];
2971
                    }
2972
                    foreach ($hookObjectsArr as $hookObj) {
2973
                        if (method_exists($hookObj, 'processCmdmap_preProcess')) {
2974
                            $hookObj->processCmdmap_preProcess($command, $table, $id, $value, $this, $pasteUpdate);
2975
                        }
2976
                    }
2977
                    // Init copyMapping array:
2978
                    // Must clear this array before call from here to those functions:
2979
                    // Contains mapping information between new and old id numbers.
2980
                    $this->copyMappingArray = [];
2981
                    // process the command
2982
                    $commandIsProcessed = false;
2983
                    foreach ($hookObjectsArr as $hookObj) {
2984
                        if (method_exists($hookObj, 'processCmdmap')) {
2985
                            $hookObj->processCmdmap($command, $table, $id, $value, $commandIsProcessed, $this, $pasteUpdate);
2986
                        }
2987
                    }
2988
                    // Only execute default commands if a hook hasn't been processed the command already
2989
                    if (!$commandIsProcessed) {
2990
                        $procId = $id;
2991
                        $backupUseTransOrigPointerField = $this->useTransOrigPointerField;
2992
                        // Branch, based on command
2993
                        switch ($command) {
2994
                            case 'move':
2995
                                $this->moveRecord($table, $id, $value);
2996
                                break;
2997
                            case 'copy':
2998
                                $target = $value['target'] ?? $value;
2999
                                $ignoreLocalization = (bool)($value['ignoreLocalization'] ?? false);
3000
                                if ($table === 'pages') {
3001
                                    $this->copyPages($id, $target);
3002
                                } else {
3003
                                    $this->copyRecord($table, $id, $target, true, [], '', 0, $ignoreLocalization);
3004
                                }
3005
                                $procId = $this->copyMappingArray[$table][$id];
3006
                                break;
3007
                            case 'localize':
3008
                                $this->useTransOrigPointerField = true;
3009
                                $this->localize($table, $id, $value);
3010
                                break;
3011
                            case 'copyToLanguage':
3012
                                $this->useTransOrigPointerField = false;
3013
                                $this->localize($table, $id, $value);
3014
                                break;
3015
                            case 'inlineLocalizeSynchronize':
3016
                                $this->inlineLocalizeSynchronize($table, $id, $value);
3017
                                break;
3018
                            case 'delete':
3019
                                $this->deleteAction($table, $id);
3020
                                break;
3021
                            case 'undelete':
3022
                                $this->undeleteRecord($table, $id);
3023
                                break;
3024
                        }
3025
                        $this->useTransOrigPointerField = $backupUseTransOrigPointerField;
3026
                        if (is_array($pasteUpdate)) {
3027
                            $pasteDatamap[$table][$procId] = $pasteUpdate;
3028
                        }
3029
                    }
3030
                    foreach ($hookObjectsArr as $hookObj) {
3031
                        if (method_exists($hookObj, 'processCmdmap_postProcess')) {
3032
                            $hookObj->processCmdmap_postProcess($command, $table, $id, $value, $this, $pasteUpdate, $pasteDatamap);
3033
                        }
3034
                    }
3035
                    // Merging the copy-array info together for remapping purposes.
3036
                    ArrayUtility::mergeRecursiveWithOverrule($this->copyMappingArray_merged, $this->copyMappingArray);
3037
                }
3038
            }
3039
        }
3040
        /** @var DataHandler $copyTCE */
3041
        $copyTCE = $this->getLocalTCE();
3042
        $copyTCE->start($pasteDatamap, [], $this->BE_USER);
3043
        $copyTCE->process_datamap();
3044
        $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3045
        unset($copyTCE);
3046
3047
        // Finally, before exit, check if there are ID references to remap.
3048
        // This might be the case if versioning or copying has taken place!
3049
        $this->remapListedDBRecords();
3050
        $this->processRemapStack();
3051
        foreach ($hookObjectsArr as $hookObj) {
3052
            if (method_exists($hookObj, 'processCmdmap_afterFinish')) {
3053
                $hookObj->processCmdmap_afterFinish($this);
3054
            }
3055
        }
3056
        if ($this->isOuterMostInstance()) {
3057
            $this->referenceIndexUpdater->update();
3058
            $this->processClearCacheQueue();
3059
            $this->resetNestedElementCalls();
3060
        }
3061
    }
3062
3063
    /*********************************************
3064
     *
3065
     * Cmd: Copying
3066
     *
3067
     ********************************************/
3068
    /**
3069
     * Copying a single record
3070
     *
3071
     * @param string $table Element table
3072
     * @param int $uid Element UID
3073
     * @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
3074
     * @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
3075
     * @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!
3076
     * @param string $excludeFields Commalist of fields to exclude from the copy process (might get default values)
3077
     * @param int $language Language ID (from sys_language table)
3078
     * @param bool $ignoreLocalization If TRUE, any localization routine is skipped
3079
     * @return int|null ID of new record, if any
3080
     * @internal should only be used from within DataHandler
3081
     */
3082
    public function copyRecord($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '', $language = 0, $ignoreLocalization = false)
3083
    {
3084
        $uid = ($origUid = (int)$uid);
3085
        // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
3086
        if (empty($GLOBALS['TCA'][$table]) || $uid === 0) {
3087
            return null;
3088
        }
3089
        if ($this->isRecordCopied($table, $uid)) {
3090
            return null;
3091
        }
3092
3093
        // Fetch record with permission check
3094
        $row = $this->recordInfoWithPermissionCheck($table, $uid, Permission::PAGE_SHOW);
3095
3096
        // This checks if the record can be selected which is all that a copy action requires.
3097
        if ($row === false) {
3098
            $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]);
3099
            return null;
3100
        }
3101
3102
        // 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...
3103
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
3104
3105
        // Check if table is allowed on destination page
3106
        if (!$this->isTableAllowedForThisPage($tscPID, $table)) {
3107
            $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]);
3108
            return null;
3109
        }
3110
3111
        $fullLanguageCheckNeeded = $table !== 'pages';
3112
        // Used to check language and general editing rights
3113
        if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded)) {
3114
            $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]);
3115
            return null;
3116
        }
3117
3118
        $data = [];
3119
        $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));
3120
        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

3120
        BackendUtility::workspaceOL($table, /** @scrutinizer ignore-type */ $row, $this->BE_USER->workspace);
Loading history...
3121
        $row = BackendUtility::purgeComputedPropertiesFromRecord($row);
3122
3123
        // Initializing:
3124
        $theNewID = StringUtility::getUniqueId('NEW');
3125
        $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3126
        $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3127
        // Getting "copy-after" fields if applicable:
3128
        $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

3128
        $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, /** @scrutinizer ignore-type */ abs($destPid), 0) : [];
Loading history...
3129
        // Page TSconfig related:
3130
        $TSConfig = BackendUtility::getPagesTSconfig($tscPID)['TCEMAIN.'] ?? [];
3131
        $tE = $this->getTableEntries($table, $TSConfig);
3132
        // Traverse ALL fields of the selected record:
3133
        foreach ($row as $field => $value) {
3134
            if (!in_array($field, $nonFields, true)) {
3135
                // Get TCA configuration for the field:
3136
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3137
                // Preparation/Processing of the value:
3138
                // "pid" is hardcoded of course:
3139
                // isset() won't work here, since values can be NULL in each of the arrays
3140
                // except setDefaultOnCopyArray, since we exploded that from a string
3141
                if ($field === 'pid') {
3142
                    $value = $destPid;
3143
                } elseif (array_key_exists($field, $overrideValues)) {
3144
                    // Override value...
3145
                    $value = $overrideValues[$field];
3146
                } elseif (array_key_exists($field, $copyAfterFields)) {
3147
                    // Copy-after value if available:
3148
                    $value = $copyAfterFields[$field];
3149
                } else {
3150
                    // Hide at copy may override:
3151
                    if ($first && $field == $enableField && $GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] && !$this->neverHideAtCopy && !$tE['disableHideAtCopy']) {
3152
                        $value = 1;
3153
                    }
3154
                    // Prepend label on copy:
3155
                    if ($first && $field == $headerField && $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] && !$tE['disablePrependAtCopy']) {
3156
                        $value = $this->getCopyHeader($table, $this->resolvePid($table, $destPid), $field, $this->clearPrefixFromValue($table, $value), 0);
3157
                    }
3158
                    // Processing based on the TCA config field type (files, references, flexforms...)
3159
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $tscPID, $language);
3160
                }
3161
                // Add value to array.
3162
                $data[$table][$theNewID][$field] = $value;
3163
            }
3164
        }
3165
        // Overriding values:
3166
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3167
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3168
        }
3169
        // Setting original UID:
3170
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3171
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3172
        }
3173
        // Do the copy by simply submitting the array through DataHandler:
3174
        /** @var DataHandler $copyTCE */
3175
        $copyTCE = $this->getLocalTCE();
3176
        $copyTCE->start($data, [], $this->BE_USER);
3177
        $copyTCE->process_datamap();
3178
        // Getting the new UID:
3179
        $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID];
3180
        if ($theNewSQLID) {
3181
            $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3182
            // Keep automatically versionized record information:
3183
            if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3184
                $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3185
            }
3186
        }
3187
        $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3188
        unset($copyTCE);
3189
        if (!$ignoreLocalization && $language == 0) {
3190
            //repointing the new translation records to the parent record we just created
3191
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3192
            if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3193
                $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = 0;
3194
            }
3195
            $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
3196
        }
3197
3198
        return $theNewSQLID;
3199
    }
3200
3201
    /**
3202
     * Copying pages
3203
     * Main function for copying pages.
3204
     *
3205
     * @param int $uid Page UID to copy
3206
     * @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
3207
     * @internal should only be used from within DataHandler
3208
     */
3209
    public function copyPages($uid, $destPid)
3210
    {
3211
        // Initialize:
3212
        $uid = (int)$uid;
3213
        $destPid = (int)$destPid;
3214
3215
        $copyTablesAlongWithPage = $this->getAllowedTablesToCopyWhenCopyingAPage();
3216
        // Begin to copy pages if we're allowed to:
3217
        if ($this->admin || in_array('pages', $copyTablesAlongWithPage, true)) {
3218
            // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)
3219
            // This method also copies the localizations of a page
3220
            $theNewRootID = $this->copySpecificPage($uid, $destPid, $copyTablesAlongWithPage, true);
3221
            // If we're going to copy recursively
3222
            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...
3223
                // Get ALL subpages to copy (read-permissions are respected!):
3224
                $CPtable = $this->int_pageTreeInfo([], $uid, (int)$this->copyTree, $theNewRootID);
3225
                // Now copying the subpages:
3226
                foreach ($CPtable as $thePageUid => $thePagePid) {
3227
                    $newPid = $this->copyMappingArray['pages'][$thePagePid];
3228
                    if (isset($newPid)) {
3229
                        $this->copySpecificPage($thePageUid, $newPid, $copyTablesAlongWithPage);
3230
                    } else {
3231
                        $this->log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Something went wrong during copying branch');
3232
                        break;
3233
                    }
3234
                }
3235
            }
3236
        } else {
3237
            $this->log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy page without permission to this table');
3238
        }
3239
    }
3240
3241
    /**
3242
     * Compile a list of tables that should be copied along when a page is about to be copied.
3243
     *
3244
     * First, get the list that the user is allowed to modify (all if admin),
3245
     * and then check against a possible limitation within "DataHandler->copyWhichTables" if not set to "*"
3246
     * to limit the list further down
3247
     *
3248
     * @return array
3249
     */
3250
    protected function getAllowedTablesToCopyWhenCopyingAPage(): array
3251
    {
3252
        // Finding list of tables to copy.
3253
        // These are the tables, the user may modify
3254
        $copyTablesArray = $this->admin ? $this->compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3255
        // If not all tables are allowed then make a list of allowed tables.
3256
        // That is the tables that figure in both allowed tables AND the copyTable-list
3257
        if (strpos($this->copyWhichTables, '*') === false) {
3258
            $definedTablesToCopy = GeneralUtility::trimExplode(',', $this->copyWhichTables, true);
3259
            // Pages are always allowed
3260
            $definedTablesToCopy[] = 'pages';
3261
            $definedTablesToCopy = array_flip($definedTablesToCopy);
3262
            foreach ($copyTablesArray as $k => $table) {
3263
                if (!$table || !isset($definedTablesToCopy[$table])) {
3264
                    unset($copyTablesArray[$k]);
3265
                }
3266
            }
3267
        }
3268
        $copyTablesArray = array_unique($copyTablesArray);
3269
        return $copyTablesArray;
3270
    }
3271
    /**
3272
     * Copying a single page ($uid) to $destPid and all tables in the array copyTablesArray.
3273
     *
3274
     * @param int $uid Page uid
3275
     * @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
3276
     * @param array $copyTablesArray Table on pages to copy along with the page.
3277
     * @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
3278
     * @return int|null The id of the new page, if applicable.
3279
     * @internal should only be used from within DataHandler
3280
     */
3281
    public function copySpecificPage($uid, $destPid, $copyTablesArray, $first = false)
3282
    {
3283
        // Copy the page itself:
3284
        $theNewRootID = $this->copyRecord('pages', $uid, $destPid, $first);
3285
        $currentWorkspaceId = (int)$this->BE_USER->workspace;
3286
        // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3287
        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...
3288
            foreach ($copyTablesArray as $table) {
3289
                // All records under the page is copied.
3290
                if ($table && is_array($GLOBALS['TCA'][$table]) && $table !== 'pages') {
3291
                    $fields = ['uid'];
3292
                    $languageField = null;
3293
                    $transOrigPointerField = null;
3294
                    $translationSourceField = null;
3295
                    if (BackendUtility::isTableLocalizable($table)) {
3296
                        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
3297
                        $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3298
                        $fields[] = $languageField;
3299
                        $fields[] = $transOrigPointerField;
3300
                        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3301
                            $translationSourceField = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3302
                            $fields[] = $translationSourceField;
3303
                        }
3304
                    }
3305
                    $isTableWorkspaceEnabled = BackendUtility::isTableWorkspaceEnabled($table);
3306
                    if ($isTableWorkspaceEnabled) {
3307
                        $fields[] = 't3ver_oid';
3308
                        $fields[] = 't3ver_state';
3309
                        $fields[] = 't3ver_wsid';
3310
                    }
3311
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3312
                    $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
3313
                    $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $currentWorkspaceId));
3314
                    $queryBuilder
3315
                        ->select(...$fields)
3316
                        ->from($table)
3317
                        ->where(
3318
                            $queryBuilder->expr()->eq(
3319
                                'pid',
3320
                                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3321
                            )
3322
                        );
3323
                    if (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby'])) {
3324
                        $queryBuilder->orderBy($GLOBALS['TCA'][$table]['ctrl']['sortby'], 'DESC');
3325
                    }
3326
                    $queryBuilder->addOrderBy('uid');
3327
                    try {
3328
                        $result = $queryBuilder->execute();
3329
                        $rows = [];
3330
                        $movedLiveIds = [];
3331
                        $movedLiveRecords = [];
3332
                        while ($row = $result->fetch()) {
3333
                            if ($isTableWorkspaceEnabled && (int)$row['t3ver_state'] === VersionState::MOVE_POINTER) {
3334
                                $movedLiveIds[(int)$row['t3ver_oid']] = (int)$row['uid'];
3335
                            }
3336
                            $rows[(int)$row['uid']] = $row;
3337
                        }
3338
                        // Resolve placeholders of workspace versions
3339
                        if (!empty($rows) && $currentWorkspaceId > 0 && $isTableWorkspaceEnabled) {
3340
                            // If a record was moved within the page, the PlainDataResolver needs the moved record
3341
                            // but not the original live version, otherwise the moved record is not considered at all.
3342
                            // For this reason, we find the live ids, where there was also a moved record in the SQL
3343
                            // query above in $movedLiveIds and now we removed them before handing them over to PlainDataResolver.
3344
                            // see changeContentSortingAndCopyDraftPage test
3345
                            foreach ($movedLiveIds as $liveId => $movePlaceHolderId) {
3346
                                if (isset($rows[$liveId])) {
3347
                                    $movedLiveRecords[$movePlaceHolderId] = $rows[$liveId];
3348
                                    unset($rows[$liveId]);
3349
                                }
3350
                            }
3351
                            $rows = array_reverse(
3352
                                $this->resolveVersionedRecords(
3353
                                    $table,
3354
                                    implode(',', $fields),
3355
                                    $GLOBALS['TCA'][$table]['ctrl']['sortby'],
3356
                                    array_keys($rows)
3357
                                ),
3358
                                true
3359
                            );
3360
                            foreach ($movedLiveRecords as $movePlaceHolderId => $liveRecord) {
3361
                                $rows[$movePlaceHolderId] = $liveRecord;
3362
                            }
3363
                        }
3364
                        if (is_array($rows)) {
3365
                            $languageSourceMap = [];
3366
                            $overrideValues = $translationSourceField ? [$translationSourceField => 0] : [];
3367
                            $doRemap = false;
3368
                            foreach ($rows as $row) {
3369
                                // Skip localized records that will be processed in
3370
                                // copyL10nOverlayRecords() on copying the default language record
3371
                                $transOrigPointer = $row[$transOrigPointerField];
3372
                                if ($row[$languageField] > 0 && $transOrigPointer > 0 && (isset($rows[$transOrigPointer]) || isset($movedLiveIds[$transOrigPointer]))) {
3373
                                    continue;
3374
                                }
3375
                                // Copying each of the underlying records...
3376
                                $newUid = $this->copyRecord($table, $row['uid'], $theNewRootID, false, $overrideValues);
3377
                                if ($translationSourceField) {
3378
                                    $languageSourceMap[$row['uid']] = $newUid;
3379
                                    if ($row[$languageField] > 0) {
3380
                                        $doRemap = true;
3381
                                    }
3382
                                }
3383
                            }
3384
                            if ($doRemap) {
3385
                                //remap is needed for records in non-default language records in the "free mode"
3386
                                $this->copy_remapTranslationSourceField($table, $rows, $languageSourceMap);
3387
                            }
3388
                        }
3389
                    } catch (DBALException $e) {
3390
                        $databaseErrorMessage = $e->getPrevious()->getMessage();
3391
                        $this->log($table, $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'An SQL error occurred: ' . $databaseErrorMessage);
3392
                    }
3393
                }
3394
            }
3395
            $this->processRemapStack();
3396
            return $theNewRootID;
3397
        }
3398
        return null;
3399
    }
3400
3401
    /**
3402
     * Copying records, but makes a "raw" copy of a record.
3403
     * Basically the only thing observed is field processing like the copying of files and correction of ids. All other fields are 1-1 copied.
3404
     * 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.
3405
     * 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!?
3406
     * This function is used to create new versions of a record.
3407
     * 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.
3408
     *
3409
     * @param string $table Element table
3410
     * @param int $uid Element UID
3411
     * @param int $pid Element PID (real PID, not checked)
3412
     * @param array $overrideArray Override array - must NOT contain any fields not in the table!
3413
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3414
     * @return int Returns the new ID of the record (if applicable)
3415
     * @internal should only be used from within DataHandler
3416
     */
3417
    public function copyRecord_raw($table, $uid, $pid, $overrideArray = [], array $workspaceOptions = [])
3418
    {
3419
        $uid = (int)$uid;
3420
        // Stop any actions if the record is marked to be deleted:
3421
        // (this can occur if IRRE elements are versionized and child elements are removed)
3422
        if ($this->isElementToBeDeleted($table, $uid)) {
3423
            return null;
3424
        }
3425
        // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3426
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
3427
            return null;
3428
        }
3429
3430
        // Fetch record with permission check
3431
        $row = $this->recordInfoWithPermissionCheck($table, $uid, Permission::PAGE_SHOW);
3432
3433
        // This checks if the record can be selected which is all that a copy action requires.
3434
        if ($row === false) {
3435
            $this->log(
3436
                $table,
3437
                $uid,
3438
                SystemLogDatabaseAction::DELETE,
3439
                0,
3440
                SystemLogErrorClassification::USER_ERROR,
3441
                'Attempt to rawcopy/versionize record which either does not exist or you don\'t have permission to read'
3442
            );
3443
            return null;
3444
        }
3445
3446
        // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3447
        $nonFields = ['uid', 'pid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody'];
3448
3449
        // Merge in override array.
3450
        $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

3450
        $row = array_merge(/** @scrutinizer ignore-type */ $row, $overrideArray);
Loading history...
3451
        // Traverse ALL fields of the selected record:
3452
        foreach ($row as $field => $value) {
3453
            if (!in_array($field, $nonFields, true)) {
3454
                // Get TCA configuration for the field:
3455
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3456
                if (is_array($conf)) {
3457
                    // Processing based on the TCA config field type (files, references, flexforms...)
3458
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3459
                }
3460
                // Add value to array.
3461
                $row[$field] = $value;
3462
            }
3463
        }
3464
        $row['pid'] = $pid;
3465
        // Setting original UID:
3466
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3467
            $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3468
        }
3469
        // Do the copy by internal function
3470
        $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3471
3472
        // When a record is copied in workspace (eg. to create a delete placeholder record for a live record), records
3473
        // pointing to that record need a reference index update. This is for instance the case in FAL, if a sys_file_reference
3474
        // for a eg. tt_content record is marked as deleted. The tt_content record then needs a reference index update.
3475
        // This scenario seems to currently only show up if in workspaces, so the refindex update is restricted to this for now.
3476
        if (!empty($workspaceOptions)) {
3477
            $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, (int)$row['uid'], (int)$this->BE_USER->workspace);
3478
        }
3479
3480
        if ($theNewSQLID) {
3481
            $this->dbAnalysisStoreExec();
3482
            $this->dbAnalysisStore = [];
3483
            return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3484
        }
3485
        return null;
3486
    }
3487
3488
    /**
3489
     * Inserts a record in the database, passing TCA configuration values through checkValue() but otherwise does NOTHING and checks nothing regarding permissions.
3490
     * 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...
3491
     *
3492
     * @param string $table Table name
3493
     * @param array $fieldArray Field array to insert as a record
3494
     * @param int $realPid The value of PID field.
3495
     * @return int Returns the new ID of the record (if applicable)
3496
     * @internal should only be used from within DataHandler
3497
     */
3498
    public function insertNewCopyVersion($table, $fieldArray, $realPid)
3499
    {
3500
        $id = StringUtility::getUniqueId('NEW');
3501
        // $fieldArray is set as current record.
3502
        // 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...
3503
        $this->checkValue_currentRecord = $fieldArray;
3504
        // Makes sure that transformations aren't processed on the copy.
3505
        $backupDontProcessTransformations = $this->dontProcessTransformations;
3506
        $this->dontProcessTransformations = true;
3507
        // Traverse record and input-process each value:
3508
        foreach ($fieldArray as $field => $fieldValue) {
3509
            if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3510
                // Evaluating the value.
3511
                $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0, $fieldArray);
3512
                if (isset($res['value'])) {
3513
                    $fieldArray[$field] = $res['value'];
3514
                }
3515
            }
3516
        }
3517
        // System fields being set:
3518
        if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3519
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3520
        }
3521
        if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3522
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3523
        }
3524
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3525
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3526
        }
3527
        // Finally, insert record:
3528
        $this->insertDB($table, $id, $fieldArray, true);
3529
        // Resets dontProcessTransformations to the previous state.
3530
        $this->dontProcessTransformations = $backupDontProcessTransformations;
3531
        // Return new id:
3532
        return $this->substNEWwithIDs[$id];
3533
    }
3534
3535
    /**
3536
     * Processing/Preparing content for copyRecord() function
3537
     *
3538
     * @param string $table Table name
3539
     * @param int $uid Record uid
3540
     * @param string $field Field name being processed
3541
     * @param string $value Input value to be processed.
3542
     * @param array $row Record array
3543
     * @param array $conf TCA field configuration
3544
     * @param int $realDestPid Real page id (pid) the record is copied to
3545
     * @param int $language Language ID (from sys_language table) used in the duplicated record
3546
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3547
     * @return array|string
3548
     * @internal
3549
     * @see copyRecord()
3550
     */
3551
    public function copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = [])
3552
    {
3553
        $inlineSubType = $this->getInlineFieldType($conf);
3554
        // Get the localization mode for the current (parent) record (keep|select):
3555
        // Register if there are references to take care of or MM is used on an inline field (no change to value):
3556
        if ($this->isReferenceField($conf) || $inlineSubType === 'mm') {
3557
            $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language);
3558
        } elseif ($inlineSubType !== false) {
3559
            $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions);
3560
        }
3561
        // 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())
3562
        if ($conf['type'] === 'flex') {
3563
            // Get current value array:
3564
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
3565
            $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
3566
                ['config' => $conf],
3567
                $table,
3568
                $field,
3569
                $row
3570
            );
3571
            $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
3572
            $currentValueArray = GeneralUtility::xml2array($value);
3573
            // Traversing the XML structure, processing files:
3574
            if (is_array($currentValueArray)) {
3575
                $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
3576
                // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3577
                $value = $currentValueArray;
3578
            }
3579
        }
3580
        return $value;
3581
    }
3582
3583
    /**
3584
     * Processes the children of an MM relation field (select, group, inline) when the parent record is copied.
3585
     *
3586
     * @param string $table
3587
     * @param int $uid
3588
     * @param string $field
3589
     * @param mixed $value
3590
     * @param array $conf
3591
     * @param string $language
3592
     * @return mixed
3593
     */
3594
    protected function copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language)
3595
    {
3596
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
3597
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
3598
        $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3599
        $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3600
        // Localize referenced records of select fields:
3601
        $localizingNonManyToManyFieldReferences = empty($mmTable) && $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3602
        /** @var RelationHandler $dbAnalysis */
3603
        $dbAnalysis = $this->createRelationHandlerInstance();
3604
        $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3605
        $purgeItems = false;
3606
        if ($language > 0 && $localizingNonManyToManyFieldReferences) {
3607
            foreach ($dbAnalysis->itemArray as $index => $item) {
3608
                // Since select fields can reference many records, check whether there's already a localization:
3609
                $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

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

3613
                    $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3614
                }
3615
            }
3616
            $purgeItems = true;
3617
        }
3618
3619
        if ($purgeItems || $mmTable) {
3620
            $dbAnalysis->purgeItemArray();
3621
            $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

3621
            $value = implode(',', $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName));
Loading history...
3622
        }
3623
        // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3624
        if ($value) {
3625
            $this->registerDBList[$table][$uid][$field] = $value;
3626
        }
3627
3628
        return $value;
3629
    }
3630
3631
    /**
3632
     * Processes child records in an inline (IRRE) element when the parent record is copied.
3633
     *
3634
     * @param string $table
3635
     * @param int $uid
3636
     * @param string $field
3637
     * @param mixed $value
3638
     * @param array $row
3639
     * @param array $conf
3640
     * @param int $realDestPid
3641
     * @param string $language
3642
     * @param array $workspaceOptions
3643
     * @return string
3644
     */
3645
    protected function copyRecord_processInline(
3646
        $table,
3647
        $uid,
3648
        $field,
3649
        $value,
3650
        $row,
3651
        $conf,
3652
        $realDestPid,
3653
        $language,
3654
        array $workspaceOptions
3655
    ) {
3656
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3657
        /** @var RelationHandler $dbAnalysis */
3658
        $dbAnalysis = $this->createRelationHandlerInstance();
3659
        $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3660
        // Walk through the items, copy them and remember the new id:
3661
        foreach ($dbAnalysis->itemArray as $k => $v) {
3662
            $newId = null;
3663
            // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3664
            if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3665
                // Children should be localized when the parent gets localized the first time, just do it:
3666
                $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

3666
                $newId = $this->localize($v['table'], $v['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3667
            } else {
3668
                if (!MathUtility::canBeInterpretedAsInteger($realDestPid)) {
3669
                    $newId = $this->copyRecord($v['table'], $v['id'], -$v['id']);
3670
                // If the destination page id is a NEW string, keep it on the same page
3671
                } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3672
                    // A filled $workspaceOptions indicated that this call
3673
                    // has it's origin in previous versionizeRecord() processing
3674
                    if (!empty($workspaceOptions)) {
3675
                        // Versions use live default id, thus the "new"
3676
                        // id is the original live default child record
3677
                        $newId = $v['id'];
3678
                        $this->versionizeRecord(
3679
                            $v['table'],
3680
                            $v['id'],
3681
                            $workspaceOptions['label'] ?? 'Auto-created for WS #' . $this->BE_USER->workspace,
3682
                            $workspaceOptions['delete'] ?? false
3683
                        );
3684
                    // Otherwise just use plain copyRecord() to create placeholders etc.
3685
                    } else {
3686
                        // If a record has been copied already during this request,
3687
                        // prevent superfluous duplication and use the existing copy
3688
                        if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3689
                            $newId = $this->copyMappingArray[$v['table']][$v['id']];
3690
                        } else {
3691
                            $newId = $this->copyRecord($v['table'], $v['id'], $realDestPid);
3692
                        }
3693
                    }
3694
                } elseif ($this->BE_USER->workspace > 0 && !BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3695
                    // We are in workspace context creating a new parent version and have a child table
3696
                    // that is not workspace aware. We don't do anything with this child.
3697
                    continue;
3698
                } else {
3699
                    // If a record has been copied already during this request,
3700
                    // prevent superfluous duplication and use the existing copy
3701
                    if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3702
                        $newId = $this->copyMappingArray[$v['table']][$v['id']];
3703
                    } else {
3704
                        $newId = $this->copyRecord_raw($v['table'], $v['id'], $realDestPid, [], $workspaceOptions);
3705
                    }
3706
                }
3707
            }
3708
            // If the current field is set on a page record, update the pid of related child records:
3709
            if ($table === 'pages') {
3710
                $this->registerDBPids[$v['table']][$v['id']] = $uid;
3711
            } elseif (isset($this->registerDBPids[$table][$uid])) {
3712
                $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][$uid];
3713
            }
3714
            $dbAnalysis->itemArray[$k]['id'] = $newId;
3715
        }
3716
        // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
3717
        $value = implode(',', $dbAnalysis->getValueArray());
3718
        $this->registerDBList[$table][$uid][$field] = $value;
3719
3720
        return $value;
3721
    }
3722
3723
    /**
3724
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
3725
     *
3726
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
3727
     * @param array $dsConf TCA field configuration (from Data Structure XML)
3728
     * @param string $dataValue The value of the flexForm field
3729
     * @param string $_1 Not used.
3730
     * @param string $_2 Not used.
3731
     * @param string $_3 Not used.
3732
     * @param array $workspaceOptions
3733
     * @return array Result array with key "value" containing the value of the processing.
3734
     * @see copyRecord()
3735
     * @see checkValue_flex_procInData_travDS()
3736
     * @internal should only be used from within DataHandler
3737
     */
3738
    public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
3739
    {
3740
        // Extract parameters:
3741
        [$table, $uid, $field, $realDestPid] = $pParams;
3742
        // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
3743
        if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
3744
            $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, [], $dsConf, $realDestPid, 0, $workspaceOptions);
3745
            $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
3746
        }
3747
        // Return
3748
        return ['value' => $dataValue];
3749
    }
3750
3751
    /**
3752
     * Find l10n-overlay records and perform the requested copy action for these records.
3753
     *
3754
     * @param string $table Record Table
3755
     * @param string $uid UID of the record in the default language
3756
     * @param string $destPid Position to copy to
3757
     * @param bool $first
3758
     * @param array $overrideValues
3759
     * @param string $excludeFields
3760
     * @internal should only be used from within DataHandler
3761
     */
3762
    public function copyL10nOverlayRecords($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '')
3763
    {
3764
        // There's no need to perform this for tables that are not localizable
3765
        if (!BackendUtility::isTableLocalizable($table)) {
3766
            return;
3767
        }
3768
3769
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3770
        $queryBuilder->getRestrictions()
3771
            ->removeAll()
3772
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3773
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
3774
3775
        $queryBuilder->select('*')
3776
            ->from($table)
3777
            ->where(
3778
                $queryBuilder->expr()->eq(
3779
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3780
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
3781
                )
3782
            );
3783
3784
        // Never copy the actual placeholders around, as the newly copied records are
3785
        // always created as new record / new placeholder pairs
3786
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
3787
            $queryBuilder->andWhere(
3788
                $queryBuilder->expr()->neq(
3789
                    't3ver_state',
3790
                    VersionState::DELETE_PLACEHOLDER
3791
                )
3792
            );
3793
        }
3794
3795
        // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
3796
        $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

3796
        $tscPID = BackendUtility::getTSconfig_pidValue($table, /** @scrutinizer ignore-type */ $uid, $destPid);
Loading history...
3797
        // Get the localized records to be copied
3798
        $l10nRecords = $queryBuilder->execute()->fetchAll();
3799
        if (is_array($l10nRecords)) {
3800
            $localizedDestPids = [];
3801
            // If $destPid < 0, then it is the uid of the original language record we are inserting after
3802
            if ($destPid < 0) {
3803
                // Get the localized records of the record we are inserting after
3804
                $queryBuilder->setParameter('pointer', abs($destPid), \PDO::PARAM_INT);
3805
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
3806
                // Index the localized record uids by language
3807
                if (is_array($destL10nRecords)) {
3808
                    foreach ($destL10nRecords as $record) {
3809
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
3810
                    }
3811
                }
3812
            }
3813
            $languageSourceMap = [
3814
                $uid => $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
3815
            ];
3816
            // Copy the localized records after the corresponding localizations of the destination record
3817
            foreach ($l10nRecords as $record) {
3818
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
3819
                if ($localizedDestPid < 0) {
3820
                    $newUid = $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
3821
                } else {
3822
                    $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

3822
                    $newUid = $this->copyRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
Loading history...
3823
                }
3824
                $languageSourceMap[$record['uid']] = $newUid;
3825
            }
3826
            $this->copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap);
3827
        }
3828
    }
3829
3830
    /**
3831
     * Remap languageSource field to uids of newly created records
3832
     *
3833
     * @param string $table Table name
3834
     * @param array $l10nRecords array of localized records from the page we're copying from (source records)
3835
     * @param array $languageSourceMap array mapping source records uids to newly copied uids
3836
     */
3837
    protected function copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap)
3838
    {
3839
        if (empty($GLOBALS['TCA'][$table]['ctrl']['translationSource']) || empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
3840
            return;
3841
        }
3842
        $translationSourceFieldName = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3843
        $translationParentFieldName = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3844
3845
        //We can avoid running these update queries by sorting the $l10nRecords by languageSource dependency (in copyL10nOverlayRecords)
3846
        //and first copy records depending on default record (and map the field).
3847
        foreach ($l10nRecords as $record) {
3848
            $oldSourceUid = $record[$translationSourceFieldName];
3849
            if ($oldSourceUid <= 0 && $record[$translationParentFieldName] > 0) {
3850
                //BC fix - in connected mode 'translationSource' field should not be 0
3851
                $oldSourceUid = $record[$translationParentFieldName];
3852
            }
3853
            if ($oldSourceUid > 0) {
3854
                if (empty($languageSourceMap[$oldSourceUid])) {
3855
                    // we don't have mapping information available e.g when copyRecord returned null
3856
                    continue;
3857
                }
3858
                $newFieldValue = $languageSourceMap[$oldSourceUid];
3859
                $updateFields = [
3860
                    $translationSourceFieldName => $newFieldValue
3861
                ];
3862
                GeneralUtility::makeInstance(ConnectionPool::class)
3863
                    ->getConnectionForTable($table)
3864
                    ->update($table, $updateFields, ['uid' => (int)$languageSourceMap[$record['uid']]]);
3865
                if ($this->BE_USER->workspace > 0) {
3866
                    GeneralUtility::makeInstance(ConnectionPool::class)
3867
                        ->getConnectionForTable($table)
3868
                        ->update($table, $updateFields, ['t3ver_oid' => (int)$languageSourceMap[$record['uid']], 't3ver_wsid' => $this->BE_USER->workspace]);
3869
                }
3870
            }
3871
        }
3872
    }
3873
3874
    /*********************************************
3875
     *
3876
     * Cmd: Moving, Localizing
3877
     *
3878
     ********************************************/
3879
    /**
3880
     * Moving single records
3881
     *
3882
     * @param string $table Table name to move
3883
     * @param int $uid Record uid to move
3884
     * @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
3885
     * @internal should only be used from within DataHandler
3886
     */
3887
    public function moveRecord($table, $uid, $destPid)
3888
    {
3889
        if (!$GLOBALS['TCA'][$table]) {
3890
            return;
3891
        }
3892
3893
        // In case the record to be moved turns out to be an offline version,
3894
        // we have to find the live version and work on that one.
3895
        if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $uid, 'uid')) {
3896
            $uid = $lookForLiveVersion['uid'];
3897
        }
3898
        // Initialize:
3899
        $destPid = (int)$destPid;
3900
        // Get this before we change the pid (for logging)
3901
        $propArr = $this->getRecordProperties($table, $uid);
3902
        $moveRec = $this->getRecordProperties($table, $uid, true);
3903
        // This is the actual pid of the moving to destination
3904
        $resolvedPid = $this->resolvePid($table, $destPid);
3905
        // 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.
3906
        // If the record is a page, then there are two options: If the page is moved within itself,
3907
        // (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.
3908
        if ($table !== 'pages' || $resolvedPid == $moveRec['pid']) {
3909
            // Edit rights for the record...
3910
            $mayMoveAccess = $this->checkRecordUpdateAccess($table, $uid);
3911
        } else {
3912
            $mayMoveAccess = $this->doesRecordExist($table, $uid, Permission::PAGE_DELETE);
3913
        }
3914
        // Finding out, if the record may be moved TO another place. Here we check insert-rights (non-pages = edit, pages = new),
3915
        // unless the pages are moved on the same pid, then edit-rights are checked
3916
        if ($table !== 'pages' || $resolvedPid != $moveRec['pid']) {
3917
            // Insert rights for the record...
3918
            $mayInsertAccess = $this->checkRecordInsertAccess($table, $resolvedPid, SystemLogDatabaseAction::MOVE);
3919
        } else {
3920
            $mayInsertAccess = $this->checkRecordUpdateAccess($table, $uid);
3921
        }
3922
        // Checking if there is anything else disallowing moving the record by checking if editing is allowed
3923
        $fullLanguageCheckNeeded = $table !== 'pages';
3924
        $mayEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded);
3925
        // If moving is allowed, begin the processing:
3926
        if (!$mayEditAccess) {
3927
            $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']);
3928
            return;
3929
        }
3930
3931
        if (!$mayMoveAccess) {
3932
            $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']);
3933
            return;
3934
        }
3935
3936
        if (!$mayInsertAccess) {
3937
            $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']);
3938
            return;
3939
        }
3940
3941
        $recordWasMoved = false;
3942
        // Move the record via a hook, used e.g. for versioning
3943
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
3944
            $hookObj = GeneralUtility::makeInstance($className);
3945
            if (method_exists($hookObj, 'moveRecord')) {
3946
                $hookObj->moveRecord($table, $uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, $this);
3947
            }
3948
        }
3949
        // Move the record if a hook hasn't moved it yet
3950
        if (!$recordWasMoved) {
0 ignored issues
show
introduced by
The condition $recordWasMoved is always false.
Loading history...
3951
            $this->moveRecord_raw($table, $uid, $destPid);
3952
        }
3953
    }
3954
3955
    /**
3956
     * Moves a record without checking security of any sort.
3957
     * USE ONLY INTERNALLY
3958
     *
3959
     * @param string $table Table name to move
3960
     * @param int $uid Record uid to move
3961
     * @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
3962
     * @see moveRecord()
3963
     * @internal should only be used from within DataHandler
3964
     */
3965
    public function moveRecord_raw($table, $uid, $destPid)
3966
    {
3967
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
3968
        $origDestPid = $destPid;
3969
        // This is the actual pid of the moving to destination
3970
        $resolvedPid = $this->resolvePid($table, $destPid);
3971
        // Checking if the pid is negative, but no sorting row is defined. In that case, find the correct pid.
3972
        // Basically this check make the error message 4-13 meaning less... But you can always remove this check if you
3973
        // prefer the error instead of a no-good action (which is to move the record to its own page...)
3974
        if (($destPid < 0 && !$sortColumn) || $destPid >= 0) {
3975
            $destPid = $resolvedPid;
3976
        }
3977
        // Get this before we change the pid (for logging)
3978
        $propArr = $this->getRecordProperties($table, $uid);
3979
        $moveRec = $this->getRecordProperties($table, $uid, true);
3980
        // Prepare user defined objects (if any) for hooks which extend this function:
3981
        $hookObjectsArr = [];
3982
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
3983
            $hookObjectsArr[] = GeneralUtility::makeInstance($className);
3984
        }
3985
        // Timestamp field:
3986
        $updateFields = [];
3987
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3988
            $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3989
        }
3990
3991
        // Check if this is a translation of a page, if so then it just needs to be kept "sorting" in sync
3992
        // Usually called from moveL10nOverlayRecords()
3993
        if ($table === 'pages') {
3994
            $defaultLanguagePageUid = $this->getDefaultLanguagePageId((int)$uid);
3995
            // In workspaces, the default language page may have been moved to a different pid than the
3996
            // default language page record of live workspace. In this case, localized pages need to be
3997
            // moved to the pid of the workspace move record.
3998
            $defaultLanguagePageWorkspaceOverlay = BackendUtility::getWorkspaceVersionOfRecord((int)$this->BE_USER->workspace, 'pages', $defaultLanguagePageUid, 'uid');
3999
            if (is_array($defaultLanguagePageWorkspaceOverlay)) {
4000
                $defaultLanguagePageUid = (int)$defaultLanguagePageWorkspaceOverlay['uid'];
4001
            }
4002
            if ($defaultLanguagePageUid !== (int)$uid) {
4003
                // If the default language page has been moved, localized pages need to be moved to
4004
                // that pid and sorting, too.
4005
                $originalTranslationRecord = $this->recordInfo($table, $defaultLanguagePageUid, 'pid,' . $sortColumn);
4006
                $updateFields[$sortColumn] = $originalTranslationRecord[$sortColumn];
4007
                $destPid = $originalTranslationRecord['pid'];
4008
            }
4009
        }
4010
4011
        // Insert as first element on page (where uid = $destPid)
4012
        if ($destPid >= 0) {
4013
            if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4014
                // Clear cache before moving
4015
                [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
4016
                $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4017
                // Setting PID
4018
                $updateFields['pid'] = $destPid;
4019
                // Table is sorted by 'sortby'
4020
                if ($sortColumn && !isset($updateFields[$sortColumn])) {
4021
                    $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4022
                    $updateFields[$sortColumn] = $sortNumber;
4023
                }
4024
                // Check for child records that have also to be moved
4025
                $this->moveRecord_procFields($table, $uid, $destPid);
4026
                // Create query for update:
4027
                GeneralUtility::makeInstance(ConnectionPool::class)
4028
                    ->getConnectionForTable($table)
4029
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4030
                // Check for the localizations of that element
4031
                $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4032
                // Call post processing hooks:
4033
                foreach ($hookObjectsArr as $hookObj) {
4034
                    if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4035
                        $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4036
                    }
4037
                }
4038
4039
                $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4040
                if ($this->enableLogging) {
4041
                    // Logging...
4042
                    $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4043
                    if ($destPid != $propArr['pid']) {
4044
                        // Logged to old page
4045
                        $newPropArr = $this->getRecordProperties($table, $uid);
4046
                        $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4047
                        $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']);
4048
                        // Logged to new page
4049
                        $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);
4050
                    } else {
4051
                        // Logged to new page
4052
                        $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);
4053
                    }
4054
                }
4055
                // Clear cache after moving
4056
                $this->registerRecordIdForPageCacheClearing($table, $uid);
4057
                $this->fixUniqueInPid($table, $uid);
4058
                $this->fixUniqueInSite($table, (int)$uid);
4059
                if ($table === 'pages') {
4060
                    $this->fixUniqueInSiteForSubpages((int)$uid);
4061
                }
4062
            } elseif ($this->enableLogging) {
4063
                $destPropArr = $this->getRecordProperties('pages', $destPid);
4064
                $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']);
4065
            }
4066
        } elseif ($sortColumn) {
4067
            // Put after another record
4068
            // Table is being sorted
4069
            // Save the position to which the original record is requested to be moved
4070
            $originalRecordDestinationPid = $destPid;
4071
            $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4072
            // Setting the destPid to the new pid of the record.
4073
            $destPid = $sortInfo['pid'];
4074
            // If not an array, there was an error (which is already logged)
4075
            if (is_array($sortInfo)) {
4076
                if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4077
                    // clear cache before moving
4078
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4079
                    // We now update the pid and sortnumber (if not set for page translations)
4080
                    $updateFields['pid'] = $destPid;
4081
                    if (!isset($updateFields[$sortColumn])) {
4082
                        $updateFields[$sortColumn] = $sortInfo['sortNumber'];
4083
                    }
4084
                    // Check for child records that have also to be moved
4085
                    $this->moveRecord_procFields($table, $uid, $destPid);
4086
                    // Create query for update:
4087
                    GeneralUtility::makeInstance(ConnectionPool::class)
4088
                        ->getConnectionForTable($table)
4089
                        ->update($table, $updateFields, ['uid' => (int)$uid]);
4090
                    // Check for the localizations of that element
4091
                    $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4092
                    // Call post processing hooks:
4093
                    foreach ($hookObjectsArr as $hookObj) {
4094
                        if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4095
                            $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4096
                        }
4097
                    }
4098
                    $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4099
                    if ($this->enableLogging) {
4100
                        // Logging...
4101
                        $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4102
                        if ($destPid != $propArr['pid']) {
4103
                            // Logged to old page
4104
                            $newPropArr = $this->getRecordProperties($table, $uid);
4105
                            $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4106
                            $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']);
4107
                            // Logged to old page
4108
                            $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);
4109
                        } else {
4110
                            // Logged to old page
4111
                            $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);
4112
                        }
4113
                    }
4114
                    // Clear cache after moving
4115
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4116
                    $this->fixUniqueInPid($table, $uid);
4117
                    $this->fixUniqueInSite($table, (int)$uid);
4118
                    if ($table === 'pages') {
4119
                        $this->fixUniqueInSiteForSubpages((int)$uid);
4120
                    }
4121
                } elseif ($this->enableLogging) {
4122
                    $destPropArr = $this->getRecordProperties('pages', $destPid);
4123
                    $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']);
4124
                }
4125
            } else {
4126
                $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']);
4127
            }
4128
        }
4129
    }
4130
4131
    /**
4132
     * Walk through all fields of the moved record and look for children of e.g. the inline type.
4133
     * If child records are found, they are also move to the new $destPid.
4134
     *
4135
     * @param string $table Record Table
4136
     * @param int $uid Record UID
4137
     * @param int $destPid Position to move to
4138
     * @internal should only be used from within DataHandler
4139
     */
4140
    public function moveRecord_procFields($table, $uid, $destPid)
4141
    {
4142
        $row = BackendUtility::getRecordWSOL($table, $uid);
4143
        if (is_array($row) && (int)$destPid !== (int)$row['pid']) {
4144
            $conf = $GLOBALS['TCA'][$table]['columns'];
4145
            foreach ($row as $field => $value) {
4146
                $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4147
            }
4148
        }
4149
    }
4150
4151
    /**
4152
     * Move child records depending on the field type of the parent record.
4153
     *
4154
     * @param string $table Record Table
4155
     * @param string $uid Record UID
4156
     * @param string $destPid Position to move to
4157
     * @param string $field Record field
4158
     * @param string $value Record field value
4159
     * @param array $conf TCA configuration of current field
4160
     * @internal should only be used from within DataHandler
4161
     */
4162
    public function moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
4163
    {
4164
        $dbAnalysis = null;
4165
        if ($conf['type'] === 'inline') {
4166
            $foreign_table = $conf['foreign_table'];
4167
            $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4168
            if ($foreign_table && $moveChildrenWithParent) {
4169
                $inlineType = $this->getInlineFieldType($conf);
4170
                if ($inlineType === 'list' || $inlineType === 'field') {
4171
                    if ($table === 'pages') {
4172
                        // If the inline elements are related to a page record,
4173
                        // make sure they reside at that page and not at its parent
4174
                        $destPid = $uid;
4175
                    }
4176
                    $dbAnalysis = $this->createRelationHandlerInstance();
4177
                    $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

4177
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
4178
                }
4179
            }
4180
        }
4181
        // Move the records
4182
        if (isset($dbAnalysis)) {
4183
            // Moving records to a positive destination will insert each
4184
            // record at the beginning, thus the order is reversed here:
4185
            foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4186
                $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

4186
                $this->moveRecord($v['table'], $v['id'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4187
            }
4188
        }
4189
    }
4190
4191
    /**
4192
     * Find l10n-overlay records and perform the requested move action for these records.
4193
     *
4194
     * @param string $table Record Table
4195
     * @param string $uid Record UID
4196
     * @param string $destPid Position to move to
4197
     * @param string $originalRecordDestinationPid Position to move the original record to
4198
     * @internal should only be used from within DataHandler
4199
     */
4200
    public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4201
    {
4202
        // There's no need to perform this for non-localizable tables
4203
        if (!BackendUtility::isTableLocalizable($table)) {
4204
            return;
4205
        }
4206
4207
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4208
        $queryBuilder->getRestrictions()
4209
            ->removeAll()
4210
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4211
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->BE_USER->workspace));
4212
4213
        $l10nRecords = $queryBuilder->select('*')
4214
            ->from($table)
4215
            ->where(
4216
                $queryBuilder->expr()->eq(
4217
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
4218
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
4219
                )
4220
            )
4221
            ->execute()
4222
            ->fetchAll();
4223
4224
        if (is_array($l10nRecords)) {
4225
            $localizedDestPids = [];
4226
            // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4227
            if ($originalRecordDestinationPid < 0) {
4228
                // Get the localized records of the record we are inserting after
4229
                $queryBuilder->setParameter('pointer', abs($originalRecordDestinationPid), \PDO::PARAM_INT);
4230
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
4231
                // Index the localized record uids by language
4232
                if (is_array($destL10nRecords)) {
4233
                    foreach ($destL10nRecords as $record) {
4234
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4235
                    }
4236
                }
4237
            }
4238
            // Move the localized records after the corresponding localizations of the destination record
4239
            foreach ($l10nRecords as $record) {
4240
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4241
                if ($localizedDestPid < 0) {
4242
                    $this->moveRecord($table, $record['uid'], $localizedDestPid);
4243
                } else {
4244
                    $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

4244
                    $this->moveRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4245
                }
4246
            }
4247
        }
4248
    }
4249
4250
    /**
4251
     * Localizes a record to another system language
4252
     *
4253
     * @param string $table Table name
4254
     * @param int $uid Record uid (to be localized)
4255
     * @param int $language Language ID (from sys_language table)
4256
     * @return int|bool The uid (int) of the new translated record or FALSE (bool) if something went wrong
4257
     * @internal should only be used from within DataHandler
4258
     */
4259
    public function localize($table, $uid, $language)
4260
    {
4261
        $newId = false;
4262
        $uid = (int)$uid;
4263
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize-' . (string)$language) !== false) {
4264
            return false;
4265
        }
4266
4267
        $this->registerNestedElementCall($table, $uid, 'localize-' . (string)$language);
4268
        if (!$GLOBALS['TCA'][$table]['ctrl']['languageField'] || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
4269
            $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table ' . $table, SystemLogErrorClassification::USER_ERROR);
4270
            return false;
4271
        }
4272
        $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4273
        if (!$langRec) {
4274
            $this->newlog('Sys language UID "' . $language . '" not found valid!', SystemLogErrorClassification::USER_ERROR);
4275
            return false;
4276
        }
4277
4278
        if (!$this->doesRecordExist($table, $uid, Permission::PAGE_SHOW)) {
4279
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' without permission.', SystemLogErrorClassification::USER_ERROR);
4280
            return false;
4281
        }
4282
4283
        // Getting workspace overlay if possible - this will localize versions in workspace if any
4284
        $row = BackendUtility::getRecordWSOL($table, $uid);
4285
        if (!is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
4286
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' that did not exist!', SystemLogErrorClassification::USER_ERROR);
4287
            return false;
4288
        }
4289
4290
        // Make sure that records which are translated from another language than the default language have a correct
4291
        // localization source set themselves, before translating them to another language.
4292
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4293
            && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0) {
4294
            $localizationParentRecord = BackendUtility::getRecord(
4295
                $table,
4296
                $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4297
            );
4298
            if ((int)$localizationParentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4299
                $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);
4300
                return false;
4301
            }
4302
        }
4303
4304
        // Default language records must never have a localization parent as they are the origin of any translation.
4305
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4306
            && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4307
            $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);
4308
            return false;
4309
        }
4310
4311
        $recordLocalizations = BackendUtility::getRecordLocalization($table, $uid, $language, 'AND pid=' . (int)$row['pid']);
4312
4313
        if (!empty($recordLocalizations)) {
4314
            $this->newlog(sprintf(
4315
                'Localization failed: there already are localizations (%s) for language %d of the "%s" record %d!',
4316
                implode(', ', array_column($recordLocalizations, 'uid')),
4317
                $language,
4318
                $table,
4319
                $uid
4320
            ), 1);
4321
            return false;
4322
        }
4323
4324
        // Initialize:
4325
        $overrideValues = [];
4326
        // Set override values:
4327
        $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $langRec['uid'];
4328
        // If the translated record is a default language record, set it's uid as localization parent of the new record.
4329
        // If translating from any other language, no override is needed; we just can copy the localization parent of
4330
        // the original record (which is pointing to the correspondent default language record) to the new record.
4331
        // In copy / free mode the TransOrigPointer field is always set to 0, as no connection to the localization parent is wanted in that case.
4332
        // For pages, there is no "copy/free mode".
4333
        if (($this->useTransOrigPointerField || $table === 'pages') && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4334
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $uid;
4335
        } elseif (!$this->useTransOrigPointerField) {
4336
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = 0;
4337
        }
4338
        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
4339
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = $uid;
4340
        }
4341
        // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4342
        if (isset($GLOBALS['TCA'][$table]['ctrl']['type'])) {
4343
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4344
        }
4345
        // Set exclude Fields:
4346
        foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fCfg) {
4347
            $translateToMsg = '';
4348
            // Check if we are just prefixing:
4349
            if ($fCfg['l10n_mode'] === 'prefixLangTitle') {
4350
                if (($fCfg['config']['type'] === 'text' || $fCfg['config']['type'] === 'input') && (string)$row[$fN] !== '') {
4351
                    [$tscPID] = BackendUtility::getTSCpid($table, $uid, '');
4352
                    $TSConfig = BackendUtility::getPagesTSconfig($tscPID)['TCEMAIN.'] ?? [];
4353
                    $tE = $this->getTableEntries($table, $TSConfig);
4354
                    if (!empty($TSConfig['translateToMessage']) && !$tE['disablePrependAtCopy']) {
4355
                        $translateToMsg = $this->getLanguageService()->sL($TSConfig['translateToMessage']);
4356
                        $translateToMsg = @sprintf($translateToMsg, $langRec['title']);
4357
                    }
4358
4359
                    foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processTranslateToClass'] ?? [] as $className) {
4360
                        $hookObj = GeneralUtility::makeInstance($className);
4361
                        if (method_exists($hookObj, 'processTranslateTo_copyAction')) {
4362
                            $hookObj->processTranslateTo_copyAction($row[$fN], $langRec, $this, $fN);
4363
                        }
4364
                    }
4365
                    if (!empty($translateToMsg)) {
4366
                        $overrideValues[$fN] = '[' . $translateToMsg . '] ' . $row[$fN];
4367
                    } else {
4368
                        $overrideValues[$fN] = $row[$fN];
4369
                    }
4370
                }
4371
            }
4372
        }
4373
4374
        if ($table !== 'pages') {
4375
            // Get the uid of record after which this localized record should be inserted
4376
            $previousUid = $this->getPreviousLocalizedRecordUid($table, $uid, $row['pid'], $language);
4377
            // Execute the copy:
4378
            $newId = $this->copyRecord($table, $uid, -$previousUid, true, $overrideValues, '', $language);
4379
        } else {
4380
            // Create new page which needs to contain the same pid as the original page
4381
            $overrideValues['pid'] = $row['pid'];
4382
            // Take over the hidden state of the original language state, this is done due to legacy reasons where-as
4383
            // pages_language_overlay was set to "hidden -> default=0" but pages hidden -> default 1"
4384
            if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
4385
                $hiddenFieldName = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
4386
                $overrideValues[$hiddenFieldName] = $row[$hiddenFieldName] ?? $GLOBALS['TCA'][$table]['columns'][$hiddenFieldName]['config']['default'];
4387
            }
4388
            $temporaryId = StringUtility::getUniqueId('NEW');
4389
            $copyTCE = $this->getLocalTCE();
4390
            $copyTCE->start([$table => [$temporaryId => $overrideValues]], [], $this->BE_USER);
4391
            $copyTCE->process_datamap();
4392
            // Getting the new UID as if it had been copied:
4393
            $theNewSQLID = $copyTCE->substNEWwithIDs[$temporaryId];
4394
            if ($theNewSQLID) {
4395
                $this->copyMappingArray[$table][$uid] = $theNewSQLID;
4396
                $newId = $theNewSQLID;
4397
            }
4398
        }
4399
4400
        return $newId;
4401
    }
4402
4403
    /**
4404
     * Performs localization or synchronization of child records.
4405
     * The $command argument expects an array, but supports a string for backward-compatibility.
4406
     *
4407
     * $command = array(
4408
     *   'field' => 'tx_myfieldname',
4409
     *   'language' => 2,
4410
     *   // either the key 'action' or 'ids' must be set
4411
     *   'action' => 'synchronize', // or 'localize'
4412
     *   'ids' => array(1, 2, 3, 4) // child element ids
4413
     * );
4414
     *
4415
     * @param string $table The table of the localized parent record
4416
     * @param int $id The uid of the localized parent record
4417
     * @param array|string $command Defines the command to be performed (see example above)
4418
     */
4419
    protected function inlineLocalizeSynchronize($table, $id, $command)
4420
    {
4421
        $parentRecord = BackendUtility::getRecordWSOL($table, $id);
4422
4423
        // Backward-compatibility handling
4424
        if (!is_array($command)) {
4425
            // <field>, (localize | synchronize | <uid>):
4426
            $parts = GeneralUtility::trimExplode(',', $command);
4427
            $command = [
4428
                'field' => $parts[0],
4429
                // The previous process expected $id to point to the localized record already
4430
                'language' => (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]
4431
            ];
4432
            if (!MathUtility::canBeInterpretedAsInteger($parts[1])) {
4433
                $command['action'] = $parts[1];
4434
            } else {
4435
                $command['ids'] = [$parts[1]];
4436
            }
4437
        }
4438
4439
        // In case the parent record is the default language record, fetch the localization
4440
        if (empty($parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
4441
            // Fetch the live record
4442
            // @todo: this needs to be revisited, as getRecordLocalization() does a BackendWorkspaceRestriction
4443
            // based on $GLOBALS[BE_USER], which could differ from the $this->BE_USER->workspace value
4444
            $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND t3ver_oid=0');
4445
            if (empty($parentRecordLocalization)) {
4446
                if ($this->enableLogging) {
4447
                    $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']));
4448
                }
4449
                return;
4450
            }
4451
            $parentRecord = $parentRecordLocalization[0];
4452
            $id = $parentRecord['uid'];
4453
            // Process overlay for current selected workspace
4454
            BackendUtility::workspaceOL($table, $parentRecord);
4455
        }
4456
4457
        $field = $command['field'];
4458
        $language = $command['language'];
4459
        $action = $command['action'];
4460
        $ids = $command['ids'];
4461
4462
        if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4463
            return;
4464
        }
4465
4466
        $config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4467
        $foreignTable = $config['foreign_table'];
4468
4469
        $transOrigPointer = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
4470
        $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
4471
4472
        if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) {
4473
            return;
4474
        }
4475
4476
        $inlineSubType = $this->getInlineFieldType($config);
4477
        if ($inlineSubType === false) {
4478
            return;
4479
        }
4480
4481
        $transOrigRecord = BackendUtility::getRecordWSOL($table, $transOrigPointer);
4482
4483
        $removeArray = [];
4484
        $mmTable = $inlineSubType === 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : '';
4485
        // Fetch children from original language parent:
4486
        /** @var RelationHandler $dbAnalysisOriginal */
4487
        $dbAnalysisOriginal = $this->createRelationHandlerInstance();
4488
        $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $table, $config);
4489
        $elementsOriginal = [];
4490
        foreach ($dbAnalysisOriginal->itemArray as $item) {
4491
            $elementsOriginal[$item['id']] = $item;
4492
        }
4493
        unset($dbAnalysisOriginal);
4494
        // Fetch children from current localized parent:
4495
        /** @var RelationHandler $dbAnalysisCurrent */
4496
        $dbAnalysisCurrent = $this->createRelationHandlerInstance();
4497
        $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
4498
        // Perform synchronization: Possibly removal of already localized records:
4499
        if ($action === 'synchronize') {
4500
            foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
4501
                $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
4502
                if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
4503
                    $childTransOrigPointer = $childRecord[$childTransOrigPointerField];
4504
                    // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it:
4505
                    if (!isset($elementsOriginal[$childTransOrigPointer])) {
4506
                        unset($dbAnalysisCurrent->itemArray[$index]);
4507
                        $removeArray[$item['table']][$item['id']]['delete'] = 1;
4508
                    }
4509
                }
4510
            }
4511
        }
4512
        // Perform synchronization/localization: Possibly add unlocalized records for original language:
4513
        if ($action === 'localize' || $action === 'synchronize') {
4514
            foreach ($elementsOriginal as $originalId => $item) {
4515
                $item['id'] = $this->localize($item['table'], $item['id'], $language);
4516
                $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4517
                $dbAnalysisCurrent->itemArray[] = $item;
4518
            }
4519
        } elseif (!empty($ids)) {
4520
            foreach ($ids as $childId) {
4521
                if (!MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) {
4522
                    continue;
4523
                }
4524
                $item = $elementsOriginal[$childId];
4525
                $item['id'] = $this->localize($item['table'], $item['id'], $language);
4526
                $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4527
                $dbAnalysisCurrent->itemArray[] = $item;
4528
            }
4529
        }
4530
        // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4531
        $value = implode(',', $dbAnalysisCurrent->getValueArray());
4532
        $this->registerDBList[$table][$id][$field] = $value;
4533
        // Remove child records (if synchronization requested it):
4534
        if (is_array($removeArray) && !empty($removeArray)) {
4535
            /** @var DataHandler $tce */
4536
            $tce = GeneralUtility::makeInstance(__CLASS__, $this->referenceIndexUpdater);
4537
            $tce->enableLogging = $this->enableLogging;
4538
            $tce->start([], $removeArray, $this->BE_USER);
4539
            $tce->process_cmdmap();
4540
            unset($tce);
4541
        }
4542
        $updateFields = [];
4543
        // Handle, reorder and store relations:
4544
        if ($inlineSubType === 'list') {
4545
            $updateFields = [$field => $value];
4546
        } elseif ($inlineSubType === 'field') {
4547
            $dbAnalysisCurrent->writeForeignField($config, $id);
4548
            $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4549
        } elseif ($inlineSubType === 'mm') {
4550
            $dbAnalysisCurrent->writeMM($config['MM'], $id);
4551
            $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4552
        }
4553
        // Update field referencing to child records of localized parent record:
4554
        if (!empty($updateFields)) {
4555
            $this->updateDB($table, $id, $updateFields);
4556
        }
4557
    }
4558
4559
    /*********************************************
4560
     *
4561
     * Cmd: Deleting
4562
     *
4563
     ********************************************/
4564
    /**
4565
     * Delete a single record
4566
     *
4567
     * @param string $table Table name
4568
     * @param int $id Record UID
4569
     * @internal should only be used from within DataHandler
4570
     */
4571
    public function deleteAction($table, $id)
4572
    {
4573
        $recordToDelete = BackendUtility::getRecord($table, $id);
4574
4575
        if (is_array($recordToDelete) && isset($recordToDelete['t3ver_wsid']) && (int)$recordToDelete['t3ver_wsid'] !== 0) {
4576
            // When dealing with a workspace record, use discard.
4577
            $this->discard($table, null, $recordToDelete);
4578
            return;
4579
        }
4580
4581
        // Record asked to be deleted was found:
4582
        if (is_array($recordToDelete)) {
4583
            $recordWasDeleted = false;
4584
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
4585
                $hookObj = GeneralUtility::makeInstance($className);
4586
                if (method_exists($hookObj, 'processCmdmap_deleteAction')) {
4587
                    $hookObj->processCmdmap_deleteAction($table, $id, $recordToDelete, $recordWasDeleted, $this);
4588
                }
4589
            }
4590
            // Delete the record if a hook hasn't deleted it yet
4591
            if (!$recordWasDeleted) {
0 ignored issues
show
introduced by
The condition $recordWasDeleted is always false.
Loading history...
4592
                $this->deleteEl($table, $id);
4593
            }
4594
        }
4595
    }
4596
4597
    /**
4598
     * Delete element from any table
4599
     *
4600
     * @param string $table Table name
4601
     * @param int $uid Record UID
4602
     * @param bool $noRecordCheck Flag: If $noRecordCheck is set, then the function does not check permission to delete record
4603
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4604
     * @param bool $deleteRecordsOnPage If false and if deleting pages, records on the page will not be deleted (edge case while swapping workspaces)
4605
     * @internal should only be used from within DataHandler
4606
     */
4607
    public function deleteEl($table, $uid, $noRecordCheck = false, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4608
    {
4609
        if ($table === 'pages') {
4610
            $this->deletePages($uid, $noRecordCheck, $forceHardDelete, $deleteRecordsOnPage);
4611
        } else {
4612
            $this->deleteVersionsForRecord($table, $uid, $forceHardDelete);
4613
            $this->deleteRecord($table, $uid, $noRecordCheck, $forceHardDelete);
4614
        }
4615
    }
4616
4617
    /**
4618
     * Delete versions for element from any table
4619
     *
4620
     * @param string $table Table name
4621
     * @param int $uid Record UID
4622
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4623
     * @internal should only be used from within DataHandler
4624
     */
4625
    public function deleteVersionsForRecord($table, $uid, $forceHardDelete)
4626
    {
4627
        $versions = BackendUtility::selectVersionsOfRecord($table, $uid, 'uid,pid,t3ver_wsid,t3ver_state', $this->BE_USER->workspace ?: null);
4628
        if (is_array($versions)) {
4629
            foreach ($versions as $verRec) {
4630
                if (!$verRec['_CURRENT_VERSION']) {
4631
                    $currentUserWorkspace = null;
4632
                    if ((int)$verRec['t3ver_wsid'] !== (int)$this->BE_USER->workspace) {
4633
                        // If deleting records from 'foreign' / 'other' workspaces, the be user must be put into
4634
                        // this workspace temporarily so stuff like refindex updating is registered for this workspace
4635
                        // when deleting records in there.
4636
                        $currentUserWorkspace = $this->BE_USER->workspace;
4637
                        $this->BE_USER->workspace = (int)$verRec['t3ver_wsid'];
4638
                    }
4639
                    if ($table === 'pages') {
4640
                        $this->deletePages($verRec['uid'], true, $forceHardDelete);
4641
                    } else {
4642
                        $this->deleteRecord($table, $verRec['uid'], true, $forceHardDelete);
4643
                    }
4644
                    if ($currentUserWorkspace !== null) {
4645
                        // Switch back workspace
4646
                        $this->BE_USER->workspace = $currentUserWorkspace;
4647
                    }
4648
                }
4649
            }
4650
        }
4651
    }
4652
4653
    /**
4654
     * Undelete a single record
4655
     *
4656
     * @param string $table Table name
4657
     * @param int $uid Record UID
4658
     * @internal should only be used from within DataHandler
4659
     */
4660
    public function undeleteRecord($table, $uid)
4661
    {
4662
        if ($this->isRecordUndeletable($table, $uid)) {
4663
            $this->deleteRecord($table, $uid, true, false, true);
4664
        }
4665
    }
4666
4667
    /**
4668
     * Deleting/Undeleting a record
4669
     * This function may not be used to delete pages-records unless the underlying records are already deleted
4670
     * Deletes a record regardless of versioning state (live or offline, doesn't matter, the uid decides)
4671
     * If both $noRecordCheck and $forceHardDelete are set it could even delete a "deleted"-flagged record!
4672
     *
4673
     * @param string $table Table name
4674
     * @param int $uid Record UID
4675
     * @param bool $noRecordCheck Flag: If $noRecordCheck is set, then the function does not check permission to delete record
4676
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4677
     * @param bool $undeleteRecord If TRUE, the "deleted" flag is set to 0 again and thus, the item is undeleted.
4678
     * @internal should only be used from within DataHandler
4679
     */
4680
    public function deleteRecord($table, $uid, $noRecordCheck = false, $forceHardDelete = false, $undeleteRecord = false)
4681
    {
4682
        $currentUserWorkspace = (int)$this->BE_USER->workspace;
4683
        $uid = (int)$uid;
4684
        if (!$GLOBALS['TCA'][$table] || !$uid) {
4685
            $this->log($table, $uid, SystemLogDatabaseAction::DELETE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to delete record without delete-permissions. [' . $this->BE_USER->errorMsg . ']');
4686
            return;
4687
        }
4688
        // Skip processing already deleted records
4689
        if (!$forceHardDelete && !$undeleteRecord && $this->hasDeletedRecord($table, $uid)) {
4690
            return;
4691
        }
4692
4693
        // Checking if there is anything else disallowing deleting the record by checking if editing is allowed
4694
        $deletedRecord = $forceHardDelete || $undeleteRecord;
4695
        $fullLanguageAccessCheck = true;
4696
        if ($table === 'pages') {
4697
            // If this is a page translation, the full language access check should not be done
4698
            $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
4699
            if ($defaultLanguagePageId !== $uid) {
4700
                $fullLanguageAccessCheck = false;
4701
            }
4702
        }
4703
        $hasEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, $deletedRecord, $fullLanguageAccessCheck);
4704
        if (!$hasEditAccess) {
4705
            $this->log($table, $uid, SystemLogDatabaseAction::DELETE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to delete record without delete-permissions');
4706
            return;
4707
        }
4708
        if ($table === 'pages') {
4709
            $perms = Permission::PAGE_DELETE;
4710
        } elseif ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
4711
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
4712
            $perms = Permission::PAGE_EDIT;
4713
        } else {
4714
            $perms = Permission::CONTENT_EDIT;
4715
        }
4716
        if (!$noRecordCheck && !$this->doesRecordExist($table, $uid, $perms)) {
4717
            return;
4718
        }
4719
4720
        // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
4721
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
4722
        $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4723
        $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
4724
        $databaseErrorMessage = '';
4725
        if ($deleteField && !$forceHardDelete) {
4726
            $updateFields = [
4727
                $deleteField => $undeleteRecord ? 0 : 1
4728
            ];
4729
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4730
                $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4731
            }
4732
            // before (un-)deleting this record, check for child records or references
4733
            $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
4734
            try {
4735
                // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
4736
                if (!$undeleteRecord) {
4737
                    $this->deletedRecords[$table][] = (int)$uid;
4738
                    $this->deleteL10nOverlayRecords($table, $uid);
4739
                }
4740
                GeneralUtility::makeInstance(ConnectionPool::class)
4741
                    ->getConnectionForTable($table)
4742
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4743
            } catch (DBALException $e) {
4744
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4745
            }
4746
        } else {
4747
            // Delete the hard way...:
4748
            try {
4749
                $this->hardDeleteSingleRecord($table, (int)$uid);
4750
                $this->deletedRecords[$table][] = (int)$uid;
4751
                $this->deleteL10nOverlayRecords($table, $uid);
4752
            } catch (DBALException $e) {
4753
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4754
            }
4755
        }
4756
        if ($this->enableLogging) {
4757
            $state = $undeleteRecord ? SystemLogDatabaseAction::INSERT : SystemLogDatabaseAction::DELETE;
4758
            if ($databaseErrorMessage === '') {
4759
                if ($forceHardDelete) {
4760
                    $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
4761
                } else {
4762
                    $message = $state === 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
4763
                }
4764
                $propArr = $this->getRecordProperties($table, $uid);
4765
                $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4766
4767
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::MESSAGE, $message, 0, [
4768
                    $propArr['header'],
4769
                    $table . ':' . $uid,
4770
                    $pagePropArr['header'],
4771
                    $propArr['pid']
4772
                ], $propArr['event_pid']);
4773
            } else {
4774
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::TODAYS_SPECIAL, $databaseErrorMessage);
4775
            }
4776
        }
4777
4778
        // Add history entry
4779
        if ($undeleteRecord) {
4780
            $this->getRecordHistoryStore()->undeleteRecord($table, $uid, $this->correlationId);
4781
        } else {
4782
            $this->getRecordHistoryStore()->deleteRecord($table, $uid, $this->correlationId);
4783
        }
4784
4785
        // Update reference index with table/uid on left side (recuid)
4786
        $this->updateRefIndex($table, $uid);
4787
        // Update reference index with table/uid on right side (ref_uid). Important if children of a relation are deleted / undeleted.
4788
        $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, $uid, $currentUserWorkspace);
4789
    }
4790
4791
    /**
4792
     * Used to delete page because it will check for branch below pages and disallowed tables on the page as well.
4793
     *
4794
     * @param int $uid Page id
4795
     * @param bool $force If TRUE, pages are not checked for permission.
4796
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4797
     * @param bool $deleteRecordsOnPage If false, records on the page will not be deleted (edge case while swapping workspaces)
4798
     * @internal should only be used from within DataHandler
4799
     */
4800
    public function deletePages($uid, $force = false, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4801
    {
4802
        $uid = (int)$uid;
4803
        if ($uid === 0) {
4804
            if ($this->enableLogging) {
4805
                $this->log('pages', $uid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'Deleting all pages starting from the root-page is disabled.', -1, [], 0);
4806
            }
4807
            return;
4808
        }
4809
        // Getting list of pages to delete:
4810
        if ($force) {
4811
            // Returns the branch WITHOUT permission checks (0 secures that), so it cannot return -1
4812
            $pageIdsInBranch = $this->doesBranchExist('', $uid, 0, true);
4813
            $res = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
4814
        } else {
4815
            $res = $this->canDeletePage($uid);
4816
        }
4817
        // Perform deletion if not error:
4818
        if (is_array($res)) {
4819
            foreach ($res as $deleteId) {
4820
                $this->deleteSpecificPage($deleteId, $forceHardDelete, $deleteRecordsOnPage);
4821
            }
4822
        } else {
4823
            /** @var FlashMessage $flashMessage */
4824
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
4825
            /** @var FlashMessageService $flashMessageService */
4826
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
4827
            $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
4828
            $this->newlog($res, SystemLogErrorClassification::USER_ERROR);
4829
        }
4830
    }
4831
4832
    /**
4833
     * Delete a page (or set deleted field to 1) and all records on it.
4834
     *
4835
     * @param int $uid Page id
4836
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4837
     * @param bool $deleteRecordsOnPage If false, records on the page will not be deleted (edge case while swapping workspaces)
4838
     * @internal
4839
     * @see deletePages()
4840
     */
4841
    public function deleteSpecificPage($uid, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4842
    {
4843
        $uid = (int)$uid;
4844
        if (!$uid) {
4845
            // Early void return on invalid uid
4846
            return;
4847
        }
4848
        $forceHardDelete = (bool)$forceHardDelete;
4849
4850
        // Delete either a default language page or a translated page
4851
        $pageIdInDefaultLanguage = $this->getDefaultLanguagePageId($uid);
4852
        $isPageTranslation = false;
4853
        $pageLanguageId = 0;
4854
        if ($pageIdInDefaultLanguage !== $uid) {
4855
            // For translated pages, translated records in other tables (eg. tt_content) for the
4856
            // to-delete translated page have their pid field set to the uid of the default language record,
4857
            // NOT the uid of the translated page record.
4858
            // If a translated page is deleted, only translations of records in other tables of this language
4859
            // should be deleted. The code checks if the to-delete page is a translated page and
4860
            // adapts the query for other tables to use the uid of the default language page as pid together
4861
            // with the language id of the translated page.
4862
            $isPageTranslation = true;
4863
            $pageLanguageId = $this->pageInfo($uid, $GLOBALS['TCA']['pages']['ctrl']['languageField']);
4864
        }
4865
4866
        if ($deleteRecordsOnPage) {
4867
            $tableNames = $this->compileAdminTables();
4868
            foreach ($tableNames as $table) {
4869
                if ($table === 'pages' || ($isPageTranslation && !BackendUtility::isTableLocalizable($table))) {
4870
                    // Skip pages table. And skip table if not translatable, but a translated page is deleted
4871
                    continue;
4872
                }
4873
4874
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4875
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
4876
                $queryBuilder
4877
                    ->select('uid')
4878
                    ->from($table);
4879
4880
                if ($isPageTranslation) {
4881
                    // Only delete records in the specified language
4882
                    $queryBuilder->where(
4883
                        $queryBuilder->expr()->eq(
4884
                            'pid',
4885
                            $queryBuilder->createNamedParameter($pageIdInDefaultLanguage, \PDO::PARAM_INT)
4886
                        ),
4887
                        $queryBuilder->expr()->eq(
4888
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
4889
                            $queryBuilder->createNamedParameter($pageLanguageId, \PDO::PARAM_INT)
4890
                        )
4891
                    );
4892
                } else {
4893
                    // Delete all records on this page
4894
                    $queryBuilder->where(
4895
                        $queryBuilder->expr()->eq(
4896
                            'pid',
4897
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
4898
                        )
4899
                    );
4900
                }
4901
4902
                $currentUserWorkspace = (int)$this->BE_USER->workspace;
4903
                if ($currentUserWorkspace !== 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
4904
                    // If we are in a workspace, make sure only records of this workspace are deleted.
4905
                    $queryBuilder->andWhere(
4906
                        $queryBuilder->expr()->eq(
4907
                            't3ver_wsid',
4908
                            $queryBuilder->createNamedParameter($currentUserWorkspace, \PDO::PARAM_INT)
4909
                        )
4910
                    );
4911
                }
4912
4913
                $statement = $queryBuilder->execute();
4914
4915
                while ($row = $statement->fetch()) {
4916
                    // Delete any further workspace overlays of the record in question, then delete the record.
4917
                    $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
4918
                    $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
4919
                }
4920
            }
4921
        }
4922
4923
        // Delete any further workspace overlays of the record in question, then delete the record.
4924
        $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
4925
        $this->deleteRecord('pages', $uid, true, $forceHardDelete);
4926
    }
4927
4928
    /**
4929
     * Used to evaluate if a page can be deleted
4930
     *
4931
     * @param int $uid Page id
4932
     * @return int[]|string If array: List of page uids to traverse and delete (means OK), if string: error message.
4933
     * @internal should only be used from within DataHandler
4934
     */
4935
    public function canDeletePage($uid)
4936
    {
4937
        $uid = (int)$uid;
4938
        $isTranslatedPage = null;
4939
4940
        // If we may at all delete this page
4941
        // If this is a page translation, do the check against the perms_* of the default page
4942
        // Because it is currently only deleting the translation
4943
        $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
4944
        if ($defaultLanguagePageId !== $uid) {
4945
            if ($this->doesRecordExist('pages', (int)$defaultLanguagePageId, Permission::PAGE_DELETE)) {
4946
                $isTranslatedPage = true;
4947
            } else {
4948
                return 'Attempt to delete page without permissions';
4949
            }
4950
        } elseif (!$this->doesRecordExist('pages', $uid, Permission::PAGE_DELETE)) {
4951
            return 'Attempt to delete page without permissions';
4952
        }
4953
4954
        $pageIdsInBranch = $this->doesBranchExist('', $uid, Permission::PAGE_DELETE, true);
4955
4956
        if ($pageIdsInBranch === -1) {
4957
            return 'Attempt to delete pages in branch without permissions';
4958
        }
4959
4960
        $pagesInBranch = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
4961
4962
        if ($disallowedTables = $this->checkForRecordsFromDisallowedTables($pagesInBranch)) {
4963
            return 'Attempt to delete records from disallowed tables (' . implode(', ', $disallowedTables) . ')';
4964
        }
4965
4966
        foreach ($pagesInBranch as $pageInBranch) {
4967
            if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, $isTranslatedPage ? false : true)) {
4968
                return 'Attempt to delete page which has prohibited localizations.';
4969
            }
4970
        }
4971
        return $pagesInBranch;
4972
    }
4973
4974
    /**
4975
     * Returns TRUE if record CANNOT be deleted, otherwise FALSE. Used to check before the versioning API allows a record to be marked for deletion.
4976
     *
4977
     * @param string $table Record Table
4978
     * @param int $id Record UID
4979
     * @return string Returns a string IF there is an error (error string explaining). FALSE means record can be deleted
4980
     * @internal should only be used from within DataHandler
4981
     */
4982
    public function cannotDeleteRecord($table, $id)
4983
    {
4984
        if ($table === 'pages') {
4985
            $res = $this->canDeletePage($id);
4986
            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...
4987
        }
4988
        if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
4989
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
4990
            $perms = Permission::PAGE_EDIT;
4991
        } else {
4992
            $perms = Permission::CONTENT_EDIT;
4993
        }
4994
        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...
4995
    }
4996
4997
    /**
4998
     * Determines whether a record can be undeleted.
4999
     *
5000
     * @param string $table Table name of the record
5001
     * @param int $uid uid of the record
5002
     * @return bool Whether the record can be undeleted
5003
     * @internal should only be used from within DataHandler
5004
     */
5005
    public function isRecordUndeletable($table, $uid)
5006
    {
5007
        $result = false;
5008
        $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5009
        if ($record['pid']) {
5010
            $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5011
            // The page containing the record is not deleted, thus the record can be undeleted:
5012
            if (!$page['deleted']) {
5013
                $result = true;
5014
            } else {
5015
                $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

5015
                $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...
5016
            }
5017
        } else {
5018
            // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5019
            $result = true;
5020
        }
5021
        return $result;
5022
    }
5023
5024
    /**
5025
     * Before a record is deleted, check if it has references such as inline type or MM references.
5026
     * If so, set these child records also to be deleted.
5027
     *
5028
     * @param string $table Record Table
5029
     * @param string $uid Record UID
5030
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5031
     * @see deleteRecord()
5032
     * @internal should only be used from within DataHandler
5033
     */
5034
    public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
5035
    {
5036
        $conf = $GLOBALS['TCA'][$table]['columns'];
5037
        $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

5037
        $row = BackendUtility::getRecord($table, /** @scrutinizer ignore-type */ $uid, '*', '', false);
Loading history...
5038
        if (empty($row)) {
5039
            return;
5040
        }
5041
        foreach ($row as $field => $value) {
5042
            $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5043
        }
5044
    }
5045
5046
    /**
5047
     * Process fields of a record to be deleted and search for special handling, like
5048
     * inline type, MM records, etc.
5049
     *
5050
     * @param string $table Record Table
5051
     * @param string $uid Record UID
5052
     * @param string $field Record field
5053
     * @param string $value Record field value
5054
     * @param array $conf TCA configuration of current field
5055
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5056
     * @see deleteRecord()
5057
     * @internal should only be used from within DataHandler
5058
     */
5059
    public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
5060
    {
5061
        if ($conf['type'] === 'inline') {
5062
            $foreign_table = $conf['foreign_table'];
5063
            if ($foreign_table) {
5064
                $inlineType = $this->getInlineFieldType($conf);
5065
                if ($inlineType === 'list' || $inlineType === 'field') {
5066
                    /** @var RelationHandler $dbAnalysis */
5067
                    $dbAnalysis = $this->createRelationHandlerInstance();
5068
                    $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

5068
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
5069
                    $dbAnalysis->undeleteRecord = true;
5070
5071
                    $enableCascadingDelete = true;
5072
                    // non type save comparison is intended!
5073
                    if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5074
                        $enableCascadingDelete = false;
5075
                    }
5076
5077
                    // Walk through the items and remove them
5078
                    foreach ($dbAnalysis->itemArray as $v) {
5079
                        if (!$undeleteRecord) {
5080
                            if ($enableCascadingDelete) {
5081
                                $this->deleteAction($v['table'], $v['id']);
5082
                            }
5083
                        } else {
5084
                            $this->undeleteRecord($v['table'], $v['id']);
5085
                        }
5086
                    }
5087
                }
5088
            }
5089
        } elseif ($this->isReferenceField($conf)) {
5090
            $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5091
            $dbAnalysis = $this->createRelationHandlerInstance();
5092
            $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5093
            foreach ($dbAnalysis->itemArray as $v) {
5094
                $this->updateRefIndex($v['table'], $v['id']);
5095
            }
5096
        }
5097
    }
5098
5099
    /**
5100
     * Find l10n-overlay records and perform the requested delete action for these records.
5101
     *
5102
     * @param string $table Record Table
5103
     * @param string $uid Record UID
5104
     * @internal should only be used from within DataHandler
5105
     */
5106
    public function deleteL10nOverlayRecords($table, $uid)
5107
    {
5108
        // Check whether table can be localized
5109
        if (!BackendUtility::isTableLocalizable($table)) {
5110
            return;
5111
        }
5112
5113
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5114
        $queryBuilder->getRestrictions()
5115
            ->removeAll()
5116
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5117
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
5118
5119
        $queryBuilder->select('*')
5120
            ->from($table)
5121
            ->where(
5122
                $queryBuilder->expr()->eq(
5123
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5124
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5125
                )
5126
            );
5127
5128
        $result = $queryBuilder->execute();
5129
        while ($record = $result->fetch()) {
5130
            // Ignore workspace delete placeholders. Those records have been marked for
5131
            // deletion before - deleting them again in a workspace would revert that state.
5132
            if ((int)$this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5133
                BackendUtility::workspaceOL($table, $record, $this->BE_USER->workspace);
5134
                if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5135
                    continue;
5136
                }
5137
            }
5138
            $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5139
        }
5140
    }
5141
5142
    /*********************************************
5143
     *
5144
     * Cmd: Workspace discard & flush
5145
     *
5146
     ********************************************/
5147
5148
    /**
5149
     * Discard a versioned record from this workspace. This deletes records from the database - no soft delete.
5150
     * This main entry method is called recursive for sub pages, localizations, relations and records on a page.
5151
     * The method checks user access and gathers facts about this record to hand the deletion over to detail methods.
5152
     *
5153
     * The incoming $uid or $row can be anything: The workspace of current user is respected and only records
5154
     * of current user workspace are discarded. If giving a live record uid, the versioned overly will be fetched.
5155
     *
5156
     * @param string $table Database table name
5157
     * @param int|null $uid Uid of live or versioned record to be discarded, or null if $record is given
5158
     * @param array|null $record Record row that should be discarded. Used instead of $uid within recursion.
5159
     */
5160
    public function discard(string $table, ?int $uid, array $record = null): void
5161
    {
5162
        if ($uid === null && $record === null) {
5163
            throw new \RuntimeException('Either record $uid or $record row must be given', 1600373491);
5164
        }
5165
5166
        // Fetch record we are dealing with if not given
5167
        if ($record === null) {
5168
            $record = BackendUtility::getRecord($table, $uid);
5169
        }
5170
        if (!is_array($record)) {
5171
            return;
5172
        }
5173
        $uid = (int)$record['uid'];
5174
5175
        // Call hook and return if hook took care of the element
5176
        $recordWasDiscarded = false;
5177
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
5178
            $hookObj = GeneralUtility::makeInstance($className);
5179
            if (method_exists($hookObj, 'processCmdmap_discardAction')) {
5180
                $hookObj->processCmdmap_discardAction($table, $uid, $record, $recordWasDiscarded);
5181
            }
5182
        }
5183
5184
        $userWorkspace = (int)$this->BE_USER->workspace;
5185
        if ($recordWasDiscarded
5186
            || $userWorkspace === 0
5187
            || !BackendUtility::isTableWorkspaceEnabled($table)
5188
            || $this->hasDeletedRecord($table, $uid)
5189
        ) {
5190
            return;
5191
        }
5192
5193
        // Gather versioned record
5194
        $versionRecord = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $versionRecord is dead and can be removed.
Loading history...
5195
        if ((int)$record['t3ver_wsid'] === 0) {
5196
            $record = BackendUtility::getWorkspaceVersionOfRecord($userWorkspace, $table, $uid);
5197
        }
5198
        if (!is_array($record)) {
5199
            return;
5200
        }
5201
        $versionRecord = $record;
5202
5203
        // User access checks
5204
        if ($userWorkspace !== (int)$versionRecord['t3ver_wsid']) {
5205
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: Different workspace', SystemLogErrorClassification::USER_ERROR);
5206
            return;
5207
        }
5208
        if ($errorCode = $this->BE_USER->workspaceCannotEditOfflineVersion($table, $versionRecord['uid'])) {
5209
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR);
5210
            return;
5211
        }
5212
        if (!$this->checkRecordUpdateAccess($table, $versionRecord['uid'])) {
5213
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: User has no edit access', SystemLogErrorClassification::USER_ERROR);
5214
            return;
5215
        }
5216
        $fullLanguageAccessCheck = !($table === 'pages' && (int)$versionRecord[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] !== 0);
5217
        if (!$this->BE_USER->recordEditAccessInternals($table, $versionRecord, false, true, $fullLanguageAccessCheck)) {
5218
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: User has no delete access', SystemLogErrorClassification::USER_ERROR);
5219
            return;
5220
        }
5221
5222
        // Perform discard operations
5223
        $versionState = VersionState::cast($versionRecord['t3ver_state']);
5224
        if ($table === 'pages' && $versionState->equals(VersionState::NEW_PLACEHOLDER)) {
5225
            // When discarding a new page, there can be new sub pages and new records.
5226
            // Those need to be discarded, otherwise they'd end up as records without parent page.
5227
            $this->discardSubPagesAndRecordsOnPage($versionRecord);
5228
        }
5229
5230
        $this->discardLocalizationOverlayRecords($table, $versionRecord);
5231
        $this->discardRecordRelations($table, $versionRecord);
5232
        $this->hardDeleteSingleRecord($table, (int)$versionRecord['uid']);
5233
        $this->deletedRecords[$table][] = (int)$versionRecord['uid'];
5234
        $this->registerReferenceIndexRowsForDrop($table, (int)$versionRecord['uid'], $userWorkspace);
5235
        $this->getRecordHistoryStore()->deleteRecord($table, (int)$versionRecord['uid'], $this->correlationId);
5236
        $this->log(
5237
            $table,
5238
            (int)$versionRecord['uid'],
5239
            SystemLogDatabaseAction::DELETE,
5240
            0,
5241
            SystemLogErrorClassification::MESSAGE,
5242
            'Record ' . $table . ':' . $versionRecord['uid'] . ' was deleted unrecoverable from page ' . $versionRecord['pid'],
5243
            0,
5244
            [],
5245
            (int)$versionRecord['pid']
5246
        );
5247
    }
5248
5249
    /**
5250
     * Also discard any sub pages and records of a new parent page if this page is discarded.
5251
     * Discarding only in specific localization, if needed.
5252
     *
5253
     * @param array $page Page record row
5254
     */
5255
    protected function discardSubPagesAndRecordsOnPage(array $page): void
5256
    {
5257
        $isLocalizedPage = false;
5258
        $sysLanguageId = (int)$page[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
5259
        $versionState = VersionState::cast($page['t3ver_state']);
5260
        if ($sysLanguageId > 0) {
5261
            // New or moved localized page.
5262
            // Discard records on this page localization, but no sub pages.
5263
            // Records of a translated page have the pid set to the default language page uid. Found in l10n_parent.
5264
            // @todo: Discard other page translations that inherit from this?! (l10n_source field)
5265
            $isLocalizedPage = true;
5266
            $pid = (int)$page[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
5267
        } elseif ($versionState->equals(VersionState::NEW_PLACEHOLDER)) {
5268
            // New default language page.
5269
            // Discard any sub pages and all other records of this page, including any page localizations.
5270
            // The t3ver_state=1 record is incoming here. Records on this page have their pid field set to the uid
5271
            // of this record. So, since t3ver_state=1 does not have an online counter-part, the actual UID is used here.
5272
            $pid = (int)$page['uid'];
5273
        } else {
5274
            // Moved default language page.
5275
            // Discard any sub pages and all other records of this page, including any page localizations.
5276
            $pid = (int)$page['t3ver_oid'];
5277
        }
5278
        $tables = $this->compileAdminTables();
5279
        foreach ($tables as $table) {
5280
            if (($isLocalizedPage && $table === 'pages')
5281
                || ($isLocalizedPage && !BackendUtility::isTableLocalizable($table))
5282
                || !BackendUtility::isTableWorkspaceEnabled($table)
5283
            ) {
5284
                continue;
5285
            }
5286
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5287
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5288
            $queryBuilder->select('*')
5289
                ->from($table)
5290
                ->where(
5291
                    $queryBuilder->expr()->eq(
5292
                        'pid',
5293
                        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
5294
                    ),
5295
                    $queryBuilder->expr()->eq(
5296
                        't3ver_wsid',
5297
                        $queryBuilder->createNamedParameter((int)$this->BE_USER->workspace, \PDO::PARAM_INT)
5298
                    )
5299
                );
5300
            if ($isLocalizedPage) {
5301
                // Add sys_language_uid = x restriction if discarding a localized page
5302
                $queryBuilder->andWhere(
5303
                    $queryBuilder->expr()->eq(
5304
                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
5305
                        $queryBuilder->createNamedParameter($sysLanguageId, \PDO::PARAM_INT)
5306
                    )
5307
                );
5308
            }
5309
            $statement = $queryBuilder->execute();
5310
            while ($row = $statement->fetch()) {
5311
                $this->discard($table, null, $row);
5312
            }
5313
        }
5314
    }
5315
5316
    /**
5317
     * Discard record relations like inline and MM of a record.
5318
     *
5319
     * @param string $table Table name of this record
5320
     * @param array $record The record row to handle
5321
     */
5322
    protected function discardRecordRelations(string $table, array $record): void
5323
    {
5324
        foreach ($record as $field => $value) {
5325
            $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? null;
5326
            if (!isset($fieldConfig['type'])) {
5327
                continue;
5328
            }
5329
            if ($fieldConfig['type'] === 'inline') {
5330
                $foreignTable = $fieldConfig['foreign_table'] ?? null;
5331
                if (!$foreignTable
5332
                     || (isset($fieldConfig['behaviour']['enableCascadingDelete'])
5333
                        && (bool)$fieldConfig['behaviour']['enableCascadingDelete'] === false)
5334
                ) {
5335
                    continue;
5336
                }
5337
                $inlineType = $this->getInlineFieldType($fieldConfig);
5338
                if ($inlineType === 'list' || $inlineType === 'field') {
5339
                    $dbAnalysis = $this->createRelationHandlerInstance();
5340
                    $dbAnalysis->start($value, $fieldConfig['foreign_table'], '', (int)$record['uid'], $table, $fieldConfig);
5341
                    $dbAnalysis->undeleteRecord = true;
5342
                    foreach ($dbAnalysis->itemArray as $relationRecord) {
5343
                        $this->discard($relationRecord['table'], (int)$relationRecord['id']);
5344
                    }
5345
                }
5346
            } elseif ($this->isReferenceField($fieldConfig) && !empty($fieldConfig['MM'])) {
5347
                $this->discardMmRelations($table, $fieldConfig, $record);
5348
            }
5349
            // @todo not inline and not mm - probably not handled correctly and has no proper test coverage yet
5350
        }
5351
    }
5352
5353
    /**
5354
     * When a workspace record row is discarded that has mm relations, existing mm table rows need
5355
     * to be deleted. The method performs the delete operation depending on TCA field configuration.
5356
     *
5357
     * @param string $table Handled table name
5358
     * @param array $fieldConfig TCA configuration of this field
5359
     * @param array $record The full record of a left- or ride-side relation
5360
     */
5361
    protected function discardMmRelations(string $table, array $fieldConfig, array $record): void
5362
    {
5363
        $recordUid = (int)$record['uid'];
5364
        $mmTableName = $fieldConfig['MM'];
5365
        // left - non foreign - uid_local vs. right - foreign - uid_foreign decision
5366
        $relationUidFieldName = isset($fieldConfig['MM_opposite_field']) ? 'uid_foreign' : 'uid_local';
5367
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($mmTableName);
5368
        $queryBuilder->delete($mmTableName)->where(
5369
            // uid_local = given uid OR uid_foreign = given uid
5370
            $queryBuilder->expr()->eq($relationUidFieldName, $queryBuilder->createNamedParameter($recordUid, \PDO::PARAM_INT))
5371
        );
5372
        if (!empty($fieldConfig['MM_table_where']) && is_string($fieldConfig['MM_table_where'])) {
5373
            $queryBuilder->andWhere(
5374
                QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$recordUid, $fieldConfig['MM_table_where']))
5375
            );
5376
        }
5377
        $mmMatchFields = $fieldConfig['MM_match_fields'] ?? [];
5378
        foreach ($mmMatchFields as $fieldName => $fieldValue) {
5379
            $queryBuilder->andWhere(
5380
                $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($fieldValue, \PDO::PARAM_STR))
5381
            );
5382
        }
5383
        $queryBuilder->execute();
5384
    }
5385
5386
    /**
5387
     * Find localization overlays of a record and discard them.
5388
     *
5389
     * @param string $table Table of this record
5390
     * @param array $record Record row
5391
     */
5392
    protected function discardLocalizationOverlayRecords(string $table, array $record): void
5393
    {
5394
        if (!BackendUtility::isTableLocalizable($table)) {
5395
            return;
5396
        }
5397
        $uid = (int)$record['uid'];
5398
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5399
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5400
        $statement = $queryBuilder->select('*')
5401
            ->from($table)
5402
            ->where(
5403
                $queryBuilder->expr()->eq(
5404
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5405
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5406
                ),
5407
                $queryBuilder->expr()->eq(
5408
                    't3ver_wsid',
5409
                    $queryBuilder->createNamedParameter((int)$this->BE_USER->workspace, \PDO::PARAM_INT)
5410
                )
5411
            )
5412
            ->execute();
5413
        while ($record = $statement->fetch()) {
5414
            $this->discard($table, null, $record);
5415
        }
5416
    }
5417
5418
    /*********************************************
5419
     *
5420
     * Cmd: Versioning
5421
     *
5422
     ********************************************/
5423
    /**
5424
     * Creates a new version of a record
5425
     * (Requires support in the table)
5426
     *
5427
     * @param string $table Table name
5428
     * @param int $id Record uid to versionize
5429
     * @param string $label Version label
5430
     * @param bool $delete If TRUE, the version is created to delete the record.
5431
     * @return int|null Returns the id of the new version (if any)
5432
     * @see copyRecord()
5433
     * @internal should only be used from within DataHandler
5434
     */
5435
    public function versionizeRecord($table, $id, $label, $delete = false)
5436
    {
5437
        $id = (int)$id;
5438
        // Stop any actions if the record is marked to be deleted:
5439
        // (this can occur if IRRE elements are versionized and child elements are removed)
5440
        if ($this->isElementToBeDeleted($table, $id)) {
5441
            return null;
5442
        }
5443
        if (!BackendUtility::isTableWorkspaceEnabled($table) || $id <= 0) {
5444
            $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, SystemLogErrorClassification::USER_ERROR);
5445
            return null;
5446
        }
5447
5448
        // Fetch record with permission check
5449
        $row = $this->recordInfoWithPermissionCheck($table, $id, Permission::PAGE_SHOW);
5450
5451
        // This checks if the record can be selected which is all that a copy action requires.
5452
        if ($row === false) {
5453
            $this->newlog(
5454
                'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "' . $table . ':' . $id . '"',
5455
                SystemLogErrorClassification::USER_ERROR
5456
            );
5457
            return null;
5458
        }
5459
5460
        // Record must be online record, otherwise we would create a version of a version
5461
        if (($row['t3ver_oid'] ?? 0) > 0) {
5462
            $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (record has an online ID)!', SystemLogErrorClassification::USER_ERROR);
5463
            return null;
5464
        }
5465
5466
        if ($delete && $this->cannotDeleteRecord($table, $id)) {
5467
            $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), SystemLogErrorClassification::USER_ERROR);
5468
            return null;
5469
        }
5470
5471
        // Set up the values to override when making a raw-copy:
5472
        $overrideArray = [
5473
            't3ver_oid' => $id,
5474
            't3ver_wsid' => $this->BE_USER->workspace,
5475
            't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5476
            't3ver_stage' => 0,
5477
        ];
5478
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5479
            $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5480
        }
5481
        // Checking if the record already has a version in the current workspace of the backend user
5482
        $versionRecord = ['uid' => null];
5483
        if ($this->BE_USER->workspace !== 0) {
5484
            // Look for version already in workspace:
5485
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5486
        }
5487
        // Create new version of the record and return the new uid
5488
        if (empty($versionRecord['uid'])) {
5489
            // Create raw-copy and return result:
5490
            // The information of the label to be used for the workspace record
5491
            // as well as the information whether the record shall be removed
5492
            // must be forwarded (creating delete placeholders on a workspace are
5493
            // done by copying the record and override several fields).
5494
            $workspaceOptions = [
5495
                'delete' => $delete,
5496
                'label' => $label,
5497
            ];
5498
            return $this->copyRecord_raw($table, $id, (int)$row['pid'], $overrideArray, $workspaceOptions);
5499
        }
5500
        // Reuse the existing record and return its uid
5501
        // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5502
        // did not make much sense since the information is available)
5503
        return $versionRecord['uid'];
5504
    }
5505
5506
    /**
5507
     * Swaps MM-relations for current/swap record, see version_swap()
5508
     *
5509
     * @param string $table Table for the two input records
5510
     * @param int $id Current record (about to go offline)
5511
     * @param int $swapWith Swap record (about to go online)
5512
     * @see version_swap()
5513
     * @internal should only be used from within DataHandler
5514
     */
5515
    public function version_remapMMForVersionSwap($table, $id, $swapWith)
5516
    {
5517
        // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5518
        $currentRec = BackendUtility::getRecord($table, $id);
5519
        $swapRec = BackendUtility::getRecord($table, $swapWith);
5520
        $this->version_remapMMForVersionSwap_reg = [];
5521
        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5522
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5523
            $conf = $fConf['config'];
5524
            if ($this->isReferenceField($conf)) {
5525
                $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5526
                $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5527
                if ($conf['MM']) {
5528
                    /** @var RelationHandler $dbAnalysis */
5529
                    $dbAnalysis = $this->createRelationHandlerInstance();
5530
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5531
                    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

5531
                    if (!empty($dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName))) {
Loading history...
5532
                        $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5533
                    }
5534
                    /** @var RelationHandler $dbAnalysis */
5535
                    $dbAnalysis = $this->createRelationHandlerInstance();
5536
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5537
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
5538
                        $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5539
                    }
5540
                }
5541
            } elseif ($conf['type'] === 'flex') {
5542
                // Current record
5543
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5544
                    $fConf,
5545
                    $table,
5546
                    $field,
5547
                    $currentRec
5548
                );
5549
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5550
                $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5551
                if (is_array($currentValueArray)) {
5552
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5553
                }
5554
                // Swap record
5555
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5556
                    $fConf,
5557
                    $table,
5558
                    $field,
5559
                    $swapRec
5560
                );
5561
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5562
                $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5563
                if (is_array($currentValueArray)) {
5564
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5565
                }
5566
            }
5567
        }
5568
        // Execute:
5569
        $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5570
    }
5571
5572
    /**
5573
     * Callback function for traversing the FlexForm structure in relation to ...
5574
     *
5575
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
5576
     * @param array $dsConf TCA field configuration (from Data Structure XML)
5577
     * @param string $dataValue The value of the flexForm field
5578
     * @param string $dataValue_ext1 Not used.
5579
     * @param string $dataValue_ext2 Not used.
5580
     * @param string $path Path in flexforms
5581
     * @see version_remapMMForVersionSwap()
5582
     * @see checkValue_flex_procInData_travDS()
5583
     * @internal should only be used from within DataHandler
5584
     */
5585
    public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
5586
    {
5587
        // Extract parameters:
5588
        [$table, $uid, $field] = $pParams;
5589
        if ($this->isReferenceField($dsConf)) {
5590
            $allowedTables = $dsConf['type'] === 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5591
            $prependName = $dsConf['type'] === 'group' ? $dsConf['prepend_tname'] : '';
5592
            if ($dsConf['MM']) {
5593
                /** @var RelationHandler $dbAnalysis */
5594
                $dbAnalysis = $this->createRelationHandlerInstance();
5595
                $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5596
                $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5597
            }
5598
        }
5599
    }
5600
5601
    /**
5602
     * Performing the remapping operations found necessary in version_remapMMForVersionSwap()
5603
     * 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.
5604
     *
5605
     * @param string $table Table for the two input records
5606
     * @param int $id Current record (about to go offline)
5607
     * @param int $swapWith Swap record (about to go online)
5608
     * @see version_remapMMForVersionSwap()
5609
     * @internal should only be used from within DataHandler
5610
     */
5611
    public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
5612
    {
5613
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5614
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5615
                $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5616
            }
5617
        }
5618
        if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5619
            foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5620
                $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5621
            }
5622
        }
5623
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5624
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5625
                $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5626
            }
5627
        }
5628
    }
5629
5630
    /*********************************************
5631
     *
5632
     * Cmd: Helper functions
5633
     *
5634
     ********************************************/
5635
5636
    /**
5637
     * Returns an instance of DataHandler for handling local datamaps/cmdmaps
5638
     *
5639
     * @return DataHandler
5640
     */
5641
    protected function getLocalTCE()
5642
    {
5643
        $copyTCE = GeneralUtility::makeInstance(DataHandler::class, $this->referenceIndexUpdater);
5644
        $copyTCE->copyTree = $this->copyTree;
5645
        $copyTCE->enableLogging = $this->enableLogging;
5646
        // Transformations should NOT be carried out during copy
5647
        $copyTCE->dontProcessTransformations = true;
5648
        // make sure the isImporting flag is transferred, so all hooks know if
5649
        // the current process is an import process
5650
        $copyTCE->isImporting = $this->isImporting;
5651
        $copyTCE->bypassAccessCheckForRecords = $this->bypassAccessCheckForRecords;
5652
        $copyTCE->bypassWorkspaceRestrictions = $this->bypassWorkspaceRestrictions;
5653
        return $copyTCE;
5654
    }
5655
5656
    /**
5657
     * Processes the fields with references as registered during the copy process. This includes all FlexForm fields which had references.
5658
     * @internal should only be used from within DataHandler
5659
     */
5660
    public function remapListedDBRecords()
5661
    {
5662
        if (!empty($this->registerDBList)) {
5663
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5664
            foreach ($this->registerDBList as $table => $records) {
5665
                foreach ($records as $uid => $fields) {
5666
                    $newData = [];
5667
                    $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5668
                    $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5669
                    foreach ($fields as $fieldName => $value) {
5670
                        $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5671
                        switch ($conf['type']) {
5672
                            case 'group':
5673
                            case 'select':
5674
                                $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5675
                                if (is_array($vArray)) {
5676
                                    $newData[$fieldName] = implode(',', $vArray);
5677
                                }
5678
                                break;
5679
                            case 'flex':
5680
                                if ($value === 'FlexForm_reference') {
5681
                                    // This will fetch the new row for the element
5682
                                    $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5683
                                    if (is_array($origRecordRow)) {
5684
                                        BackendUtility::workspaceOL($table, $origRecordRow);
5685
                                        // Get current data structure and value array:
5686
                                        $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5687
                                            ['config' => $conf],
5688
                                            $table,
5689
                                            $fieldName,
5690
                                            $origRecordRow
5691
                                        );
5692
                                        $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5693
                                        $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5694
                                        // Do recursive processing of the XML data:
5695
                                        $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5696
                                        // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5697
                                        if (is_array($currentValueArray['data'])) {
5698
                                            $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5699
                                        }
5700
                                    }
5701
                                }
5702
                                break;
5703
                            case 'inline':
5704
                                $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5705
                                break;
5706
                            default:
5707
                                $this->logger->debug('Field type should not appear here: ' . $conf['type']);
5708
                        }
5709
                    }
5710
                    // If any fields were changed, those fields are updated!
5711
                    if (!empty($newData)) {
5712
                        $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5713
                    }
5714
                }
5715
            }
5716
        }
5717
    }
5718
5719
    /**
5720
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
5721
     *
5722
     * @param array $pParams Set of parameters in numeric array: table, uid, field
5723
     * @param array $dsConf TCA config for field (from Data Structure of course)
5724
     * @param string $dataValue Field value (from FlexForm XML)
5725
     * @param string $dataValue_ext1 Not used
5726
     * @param string $dataValue_ext2 Not used
5727
     * @return array Array where the "value" key carries the value.
5728
     * @see checkValue_flex_procInData_travDS()
5729
     * @see remapListedDBRecords()
5730
     * @internal should only be used from within DataHandler
5731
     */
5732
    public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
5733
    {
5734
        // Extract parameters:
5735
        [$table, $uid, $field] = $pParams;
5736
        // If references are set for this field, set flag so they can be corrected later:
5737
        if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5738
            $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5739
            if (is_array($vArray)) {
5740
                $dataValue = implode(',', $vArray);
5741
            }
5742
        }
5743
        // Return
5744
        return ['value' => $dataValue];
5745
    }
5746
5747
    /**
5748
     * Performs remapping of old UID values to NEW uid values for a DB reference field.
5749
     *
5750
     * @param array $conf TCA field config
5751
     * @param string $value Field value
5752
     * @param int $MM_localUid UID of local record (for MM relations - might need to change if support for FlexForms should be done!)
5753
     * @param string $table Table name
5754
     * @return array|null Returns array of items ready to implode for field content.
5755
     * @see remapListedDBRecords()
5756
     * @internal should only be used from within DataHandler
5757
     */
5758
    public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
5759
    {
5760
        // Initialize variables
5761
        // Will be set TRUE if an upgrade should be done...
5762
        $set = false;
5763
        // Allowed tables for references.
5764
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5765
        // Table name to prepend the UID
5766
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5767
        // Which tables that should possibly not be remapped
5768
        $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5769
        // Convert value to list of references:
5770
        $dbAnalysis = $this->createRelationHandlerInstance();
5771
        $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && $conf['allowNonIdValues'];
5772
        $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5773
        // Traverse those references and map IDs:
5774
        foreach ($dbAnalysis->itemArray as $k => $v) {
5775
            $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5776
            if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5777
                $dbAnalysis->itemArray[$k]['id'] = $mapID;
5778
                $set = true;
5779
            }
5780
        }
5781
        if (!empty($conf['MM'])) {
5782
            // Purge invalid items (live/version)
5783
            $dbAnalysis->purgeItemArray();
5784
            if ($dbAnalysis->isPurged()) {
5785
                $set = true;
5786
            }
5787
5788
            // If record has been versioned/copied in this process, handle invalid relations of the live record
5789
            $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5790
            $originalId = 0;
5791
            if (!empty($this->copyMappingArray_merged[$table])) {
5792
                $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5793
            }
5794
            if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5795
                $liveRelations = $this->createRelationHandlerInstance();
5796
                $liveRelations->setWorkspaceId(0);
5797
                $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5798
                // Purge invalid relations in the live workspace ("0")
5799
                $liveRelations->purgeItemArray(0);
5800
                if ($liveRelations->isPurged()) {
5801
                    $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

5801
                    $liveRelations->writeMM($conf['MM'], $liveId, /** @scrutinizer ignore-type */ $prependName);
Loading history...
5802
                }
5803
            }
5804
        }
5805
        // If a change has been done, set the new value(s)
5806
        if ($set) {
5807
            if ($conf['MM']) {
5808
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5809
            } else {
5810
                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

5810
                return $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName);
Loading history...
5811
            }
5812
        }
5813
        return null;
5814
    }
5815
5816
    /**
5817
     * Performs remapping of old UID values to NEW uid values for an inline field.
5818
     *
5819
     * @param array $conf TCA field config
5820
     * @param string $value Field value
5821
     * @param int $uid The uid of the ORIGINAL record
5822
     * @param string $table Table name
5823
     * @internal should only be used from within DataHandler
5824
     */
5825
    public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
5826
    {
5827
        $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5828
        if ($conf['foreign_table']) {
5829
            $inlineType = $this->getInlineFieldType($conf);
5830
            if ($inlineType === 'mm') {
5831
                $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5832
            } elseif ($inlineType !== false) {
5833
                /** @var RelationHandler $dbAnalysis */
5834
                $dbAnalysis = $this->createRelationHandlerInstance();
5835
                $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5836
5837
                $updatePidForRecords = [];
5838
                // Update values for specific versioned records
5839
                foreach ($dbAnalysis->itemArray as &$item) {
5840
                    $updatePidForRecords[$item['table']][] = $item['id'];
5841
                    $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5842
                    if ($versionedId !== null) {
5843
                        $updatePidForRecords[$item['table']][] = $versionedId;
5844
                        $item['id'] = $versionedId;
5845
                    }
5846
                }
5847
5848
                // Update child records if using pointer fields ('foreign_field'):
5849
                if ($inlineType === 'field') {
5850
                    $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
5851
                }
5852
                $thePidToUpdate = null;
5853
                // If the current field is set on a page record, update the pid of related child records:
5854
                if ($table === 'pages') {
5855
                    $thePidToUpdate = $theUidToUpdate;
5856
                } elseif (isset($this->registerDBPids[$table][$uid])) {
5857
                    $thePidToUpdate = $this->registerDBPids[$table][$uid];
5858
                    $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
5859
                }
5860
5861
                // Update child records if change to pid is required
5862
                if ($thePidToUpdate && !empty($updatePidForRecords)) {
5863
                    // Ensure that only the default language page is used as PID
5864
                    $thePidToUpdate = $this->getDefaultLanguagePageId($thePidToUpdate);
5865
                    // @todo: this can probably go away
5866
                    // ensure, only live page ids are used as 'pid' values
5867
                    $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
5868
                    if ($liveId !== null) {
5869
                        $thePidToUpdate = $liveId;
5870
                    }
5871
                    $updateValues = ['pid' => $thePidToUpdate];
5872
                    foreach ($updatePidForRecords as $tableName => $uids) {
5873
                        if (empty($tableName) || empty($uids)) {
5874
                            continue;
5875
                        }
5876
                        $conn = GeneralUtility::makeInstance(ConnectionPool::class)
5877
                            ->getConnectionForTable($tableName);
5878
                        foreach ($uids as $updateUid) {
5879
                            $conn->update($tableName, $updateValues, ['uid' => $updateUid]);
5880
                        }
5881
                    }
5882
                }
5883
            }
5884
        }
5885
    }
5886
5887
    /**
5888
     * Processes the $this->remapStack at the end of copying, inserting, etc. actions.
5889
     * The remapStack takes care about the correct mapping of new and old uids in case of relational data.
5890
     * @internal should only be used from within DataHandler
5891
     */
5892
    public function processRemapStack()
5893
    {
5894
        // Processes the remap stack:
5895
        if (is_array($this->remapStack)) {
0 ignored issues
show
introduced by
The condition is_array($this->remapStack) is always true.
Loading history...
5896
            $remapFlexForms = [];
5897
            $hookPayload = [];
5898
5899
            $newValue = null;
5900
            foreach ($this->remapStack as $remapAction) {
5901
                // If no position index for the arguments was set, skip this remap action:
5902
                if (!is_array($remapAction['pos'])) {
5903
                    continue;
5904
                }
5905
                // Load values from the argument array in remapAction:
5906
                $field = $remapAction['field'];
5907
                $id = $remapAction['args'][$remapAction['pos']['id']];
5908
                $rawId = $id;
5909
                $table = $remapAction['args'][$remapAction['pos']['table']];
5910
                $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
5911
                $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
5912
                $additionalData = $remapAction['additionalData'];
5913
                // The record is new and has one or more new ids (in case of versioning/workspaces):
5914
                if (strpos($id, 'NEW') !== false) {
5915
                    // Replace NEW...-ID with real uid:
5916
                    $id = $this->substNEWwithIDs[$id];
5917
                    // If the new parent record is on a non-live workspace or versionized, it has another new id:
5918
                    if (isset($this->autoVersionIdMap[$table][$id])) {
5919
                        $id = $this->autoVersionIdMap[$table][$id];
5920
                    }
5921
                    $remapAction['args'][$remapAction['pos']['id']] = $id;
5922
                }
5923
                // Replace relations to NEW...-IDs in field value (uids of child records):
5924
                if (is_array($valueArray)) {
5925
                    foreach ($valueArray as $key => $value) {
5926
                        if (strpos($value, 'NEW') !== false) {
5927
                            if (strpos($value, '_') === false) {
5928
                                $affectedTable = $tcaFieldConf['foreign_table'];
5929
                                $prependTable = false;
5930
                            } else {
5931
                                $parts = explode('_', $value);
5932
                                $value = array_pop($parts);
5933
                                $affectedTable = implode('_', $parts);
5934
                                $prependTable = true;
5935
                            }
5936
                            $value = $this->substNEWwithIDs[$value];
5937
                            // The record is new, but was also auto-versionized and has another new id:
5938
                            if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
5939
                                $value = $this->autoVersionIdMap[$affectedTable][$value];
5940
                            }
5941
                            if ($prependTable) {
5942
                                $value = $affectedTable . '_' . $value;
5943
                            }
5944
                            // Set a hint that this was a new child record:
5945
                            $this->newRelatedIDs[$affectedTable][] = $value;
5946
                            $valueArray[$key] = $value;
5947
                        }
5948
                    }
5949
                    $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
5950
                }
5951
                // Process the arguments with the defined function:
5952
                if (!empty($remapAction['func'])) {
5953
                    $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
5954
                }
5955
                // If array is returned, check for maxitems condition, if string is returned this was already done:
5956
                if (is_array($newValue)) {
5957
                    $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
5958
                    // The reference casting is only required if
5959
                    // checkValue_group_select_processDBdata() returns an array
5960
                    $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
5961
                }
5962
                // Update in database (list of children (csv) or number of relations (foreign_field)):
5963
                if (!empty($field)) {
5964
                    $fieldArray = [$field => $newValue];
5965
                    if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
5966
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
5967
                    }
5968
                    $this->updateDB($table, $id, $fieldArray);
5969
                } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
5970
                    // Collect data to update FlexForms
5971
                    $flexFormId = $additionalData['flexFormId'];
5972
                    $flexFormPath = $additionalData['flexFormPath'];
5973
5974
                    if (!isset($remapFlexForms[$flexFormId])) {
5975
                        $remapFlexForms[$flexFormId] = [];
5976
                    }
5977
5978
                    $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
5979
                }
5980
5981
                // Collect elements that shall trigger processDatamap_afterDatabaseOperations
5982
                if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
5983
                    $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
5984
                    if (!isset($hookPayload[$table][$rawId])) {
5985
                        $hookPayload[$table][$rawId] = [
5986
                            'status' => $hookArgs['status'],
5987
                            'fieldArray' => $hookArgs['fieldArray'],
5988
                            'hookObjects' => $hookArgs['hookObjectsArr'],
5989
                        ];
5990
                    }
5991
                    $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
5992
                }
5993
            }
5994
5995
            if ($remapFlexForms) {
5996
                foreach ($remapFlexForms as $flexFormId => $modifications) {
5997
                    $this->updateFlexFormData($flexFormId, $modifications);
5998
                }
5999
            }
6000
6001
            foreach ($hookPayload as $tableName => $rawIdPayload) {
6002
                foreach ($rawIdPayload as $rawId => $payload) {
6003
                    foreach ($payload['hookObjects'] as $hookObject) {
6004
                        if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
6005
                            continue;
6006
                        }
6007
                        $hookObject->processDatamap_afterDatabaseOperations(
6008
                            $payload['status'],
6009
                            $tableName,
6010
                            $rawId,
6011
                            $payload['fieldArray'],
6012
                            $this
6013
                        );
6014
                    }
6015
                }
6016
            }
6017
        }
6018
        // Processes the remap stack actions:
6019
        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...
6020
            foreach ($this->remapStackActions as $action) {
6021
                if (isset($action['callback'], $action['arguments'])) {
6022
                    call_user_func_array($action['callback'], $action['arguments']);
6023
                }
6024
            }
6025
        }
6026
        // Reset:
6027
        $this->remapStack = [];
6028
        $this->remapStackRecords = [];
6029
        $this->remapStackActions = [];
6030
    }
6031
6032
    /**
6033
     * Updates FlexForm data.
6034
     *
6035
     * @param string $flexFormId e.g. <table>:<uid>:<field>
6036
     * @param array $modifications Modifications with paths and values (e.g. 'sDEF/lDEV/field/vDEF' => 'TYPO3')
6037
     */
6038
    protected function updateFlexFormData($flexFormId, array $modifications)
6039
    {
6040
        [$table, $uid, $field] = explode(':', $flexFormId, 3);
6041
6042
        if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
6043
            $uid = $this->substNEWwithIDs[$uid];
6044
        }
6045
6046
        $record = $this->recordInfo($table, $uid, '*');
6047
6048
        if (!$table || !$uid || !$field || !is_array($record)) {
6049
            return;
6050
        }
6051
6052
        BackendUtility::workspaceOL($table, $record);
6053
6054
        // Get current data structure and value array:
6055
        $valueStructure = GeneralUtility::xml2array($record[$field]);
6056
6057
        // Do recursive processing of the XML data:
6058
        foreach ($modifications as $path => $value) {
6059
            $valueStructure['data'] = ArrayUtility::setValueByPath(
6060
                $valueStructure['data'],
6061
                $path,
6062
                $value
6063
            );
6064
        }
6065
6066
        if (is_array($valueStructure['data'])) {
6067
            // The return value should be compiled back into XML
6068
            $values = [
6069
                $field => $this->checkValue_flexArray2Xml($valueStructure, true),
6070
            ];
6071
6072
            $this->updateDB($table, $uid, $values);
6073
        }
6074
    }
6075
6076
    /**
6077
     * Adds an instruction to the remap action stack (used with IRRE).
6078
     *
6079
     * @param string $table The affected table
6080
     * @param int $id The affected ID
6081
     * @param array $callback The callback information (object and method)
6082
     * @param array $arguments The arguments to be used with the callback
6083
     * @internal should only be used from within DataHandler
6084
     */
6085
    public function addRemapAction($table, $id, array $callback, array $arguments)
6086
    {
6087
        $this->remapStackActions[] = [
6088
            'affects' => [
6089
                'table' => $table,
6090
                'id' => $id
6091
            ],
6092
            'callback' => $callback,
6093
            'arguments' => $arguments
6094
        ];
6095
    }
6096
6097
    /**
6098
     * If a parent record was versionized on a workspace in $this->process_datamap,
6099
     * it might be possible, that child records (e.g. on using IRRE) were affected.
6100
     * This function finds these relations and updates their uids in the $incomingFieldArray.
6101
     * The $incomingFieldArray is updated by reference!
6102
     *
6103
     * @param string $table Table name of the parent record
6104
     * @param int $id Uid of the parent record
6105
     * @param array $incomingFieldArray Reference to the incomingFieldArray of process_datamap
6106
     * @param array $registerDBList Reference to the $registerDBList array that was created/updated by versionizing calls to DataHandler in process_datamap.
6107
     * @internal should only be used from within DataHandler
6108
     */
6109
    public function getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
6110
    {
6111
        if (is_array($registerDBList[$table][$id])) {
6112
            foreach ($incomingFieldArray as $field => $value) {
6113
                $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
6114
                if ($registerDBList[$table][$id][$field] && ($foreignTable = $fieldConf['foreign_table'])) {
6115
                    $newValueArray = [];
6116
                    $origValueArray = is_array($value) ? $value : explode(',', $value);
6117
                    // Update the uids of the copied records, but also take care about new records:
6118
                    foreach ($origValueArray as $childId) {
6119
                        $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ?: $childId;
6120
                    }
6121
                    // Set the changed value to the $incomingFieldArray
6122
                    $incomingFieldArray[$field] = implode(',', $newValueArray);
6123
                }
6124
            }
6125
            // Clean up the $registerDBList array:
6126
            unset($registerDBList[$table][$id]);
6127
            if (empty($registerDBList[$table])) {
6128
                unset($registerDBList[$table]);
6129
            }
6130
        }
6131
    }
6132
6133
    /**
6134
     * Simple helper method to hard delete one row from table ignoring delete TCA field
6135
     *
6136
     * @param string $table A row from this table should be deleted
6137
     * @param int $uid Uid of row to be deleted
6138
     */
6139
    protected function hardDeleteSingleRecord(string $table, int $uid): void
6140
    {
6141
        GeneralUtility::makeInstance(ConnectionPool::class)
6142
            ->getConnectionForTable($table)
6143
            ->delete($table, ['uid' => $uid], [\PDO::PARAM_INT]);
6144
    }
6145
6146
    /*****************************
6147
     *
6148
     * Access control / Checking functions
6149
     *
6150
     *****************************/
6151
    /**
6152
     * Checking group modify_table access list
6153
     *
6154
     * @param string $table Table name
6155
     * @return bool Returns TRUE if the user has general access to modify the $table
6156
     * @internal should only be used from within DataHandler
6157
     */
6158
    public function checkModifyAccessList($table)
6159
    {
6160
        $res = $this->admin || (!$this->tableAdminOnly($table) && isset($this->BE_USER->groupData['tables_modify']) && GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table));
6161
        // Hook 'checkModifyAccessList': Post-processing of the state of access
6162
        foreach ($this->getCheckModifyAccessListHookObjects() as $hookObject) {
6163
            /** @var DataHandlerCheckModifyAccessListHookInterface $hookObject */
6164
            $hookObject->checkModifyAccessList($res, $table, $this);
6165
        }
6166
        return $res;
6167
    }
6168
6169
    /**
6170
     * Checking if a record with uid $id from $table is in the BE_USERS webmounts which is required for editing etc.
6171
     *
6172
     * @param string $table Table name
6173
     * @param int $id UID of record
6174
     * @return bool Returns TRUE if OK. Cached results.
6175
     * @internal should only be used from within DataHandler
6176
     */
6177
    public function isRecordInWebMount($table, $id)
6178
    {
6179
        if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
6180
            $recP = $this->getRecordProperties($table, $id);
6181
            $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
6182
        }
6183
        return $this->isRecordInWebMount_Cache[$table . ':' . $id];
6184
    }
6185
6186
    /**
6187
     * Checks if the input page ID is in the BE_USER webmounts
6188
     *
6189
     * @param int $pid Page ID to check
6190
     * @return bool TRUE if OK. Cached results.
6191
     * @internal should only be used from within DataHandler
6192
     */
6193
    public function isInWebMount($pid)
6194
    {
6195
        if (!isset($this->isInWebMount_Cache[$pid])) {
6196
            $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
6197
        }
6198
        return $this->isInWebMount_Cache[$pid];
6199
    }
6200
6201
    /**
6202
     * Checks if user may update a record with uid=$id from $table
6203
     *
6204
     * @param string $table Record table
6205
     * @param int $id Record UID
6206
     * @param array|bool $data Record data
6207
     * @param array $hookObjectsArr Hook objects
6208
     * @return bool Returns TRUE if the user may update the record given by $table and $id
6209
     * @internal should only be used from within DataHandler
6210
     */
6211
    public function checkRecordUpdateAccess($table, $id, $data = false, $hookObjectsArr = null)
6212
    {
6213
        $res = null;
6214
        if (is_array($hookObjectsArr)) {
6215
            foreach ($hookObjectsArr as $hookObj) {
6216
                if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
6217
                    $res = $hookObj->checkRecordUpdateAccess($table, $id, $data, $res, $this);
6218
                }
6219
            }
6220
            if (isset($res)) {
6221
                return (bool)$res;
6222
            }
6223
        }
6224
        $res = false;
6225
6226
        if ($GLOBALS['TCA'][$table] && (int)$id > 0) {
6227
            $cacheId = 'checkRecordUpdateAccess_' . $table . '_' . $id;
6228
6229
            // If information is cached, return it
6230
            $cachedValue = $this->runtimeCache->get($cacheId);
6231
            if (!empty($cachedValue)) {
6232
                return $cachedValue;
6233
            }
6234
6235
            if ($table === 'pages' || ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap))) {
6236
                // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6237
                $perms = Permission::PAGE_EDIT;
6238
            } else {
6239
                $perms = Permission::CONTENT_EDIT;
6240
            }
6241
            if ($this->doesRecordExist($table, $id, $perms)) {
6242
                $res = 1;
6243
            }
6244
            // Cache the result
6245
            $this->runtimeCache->set($cacheId, $res);
6246
        }
6247
        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...
6248
    }
6249
6250
    /**
6251
     * Checks if user may insert a record from $insertTable on $pid
6252
     *
6253
     * @param string $insertTable Tablename to check
6254
     * @param int $pid Integer PID
6255
     * @param int $action For logging: Action number.
6256
     * @return bool Returns TRUE if the user may insert a record from table $insertTable on page $pid
6257
     * @internal should only be used from within DataHandler
6258
     */
6259
    public function checkRecordInsertAccess($insertTable, $pid, $action = SystemLogDatabaseAction::INSERT)
6260
    {
6261
        $pid = (int)$pid;
6262
        if ($pid < 0) {
6263
            return false;
6264
        }
6265
        // If information is cached, return it
6266
        if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
6267
            return $this->recInsertAccessCache[$insertTable][$pid];
6268
        }
6269
6270
        $res = false;
6271
        if ($insertTable === 'pages') {
6272
            $perms = Permission::PAGE_NEW;
6273
        } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
6274
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6275
            $perms = Permission::PAGE_EDIT;
6276
        } else {
6277
            $perms = Permission::CONTENT_EDIT;
6278
        }
6279
        $pageExists = (bool)$this->doesRecordExist('pages', $pid, $perms);
6280
        // 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
6281
        if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
6282
            // Check permissions
6283
            if ($this->isTableAllowedForThisPage($pid, $insertTable)) {
6284
                $res = true;
6285
                // Cache the result
6286
                $this->recInsertAccessCache[$insertTable][$pid] = $res;
6287
            } elseif ($this->enableLogging) {
6288
                $propArr = $this->getRecordProperties('pages', $pid);
6289
                $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']);
6290
            }
6291
        } elseif ($this->enableLogging) {
6292
            $propArr = $this->getRecordProperties('pages', $pid);
6293
            $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']);
6294
        }
6295
        return $res;
6296
    }
6297
6298
    /**
6299
     * 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.
6300
     *
6301
     * @param int $page_uid Page id for which to check, including 0 (zero) if checking for page tree root.
6302
     * @param string $checkTable Table name to check
6303
     * @return bool TRUE if OK
6304
     * @internal should only be used from within DataHandler
6305
     */
6306
    public function isTableAllowedForThisPage($page_uid, $checkTable)
6307
    {
6308
        $page_uid = (int)$page_uid;
6309
        $rootLevelSetting = (int)$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'];
6310
        // 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.
6311
        if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$page_uid)) {
6312
            return false;
6313
        }
6314
        $allowed = false;
6315
        // Check root-level
6316
        if (!$page_uid) {
6317
            if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
6318
                $allowed = true;
6319
            }
6320
        } else {
6321
            // Check non-root-level
6322
            $doktype = $this->pageInfo($page_uid, 'doktype');
6323
            $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6324
            $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6325
            // If all tables or the table is listed as an allowed type, return TRUE
6326
            if (strpos($allowedTableList, '*') !== false || in_array($checkTable, $allowedArray, true)) {
6327
                $allowed = true;
6328
            }
6329
        }
6330
        return $allowed;
6331
    }
6332
6333
    /**
6334
     * Checks if record can be selected based on given permission criteria
6335
     *
6336
     * @param string $table Record table name
6337
     * @param int $id Record UID
6338
     * @param int $perms Permission restrictions to observe: integer that will be bitwise AND'ed.
6339
     * @return bool Returns TRUE if the record given by $table, $id and $perms can be selected
6340
     *
6341
     * @throws \RuntimeException
6342
     * @internal should only be used from within DataHandler
6343
     */
6344
    public function doesRecordExist($table, $id, int $perms)
6345
    {
6346
        return $this->recordInfoWithPermissionCheck($table, $id, $perms, 'uid, pid') !== false;
6347
    }
6348
6349
    /**
6350
     * Looks up a page based on permissions.
6351
     *
6352
     * @param int $id Page id
6353
     * @param int $perms Permission integer
6354
     * @param array $columns Columns to select
6355
     * @return bool|array
6356
     * @internal
6357
     * @see doesRecordExist()
6358
     */
6359
    protected function doesRecordExist_pageLookUp($id, $perms, $columns = ['uid'])
6360
    {
6361
        $permission = new Permission($perms);
6362
        $cacheId = md5('doesRecordExist_pageLookUp_' . $id . '_' . $perms . '_' . implode(
6363
            '_',
6364
            $columns
6365
        ) . '_' . (string)$this->admin);
6366
6367
        // If result is cached, return it
6368
        $cachedResult = $this->runtimeCache->get($cacheId);
6369
        if (!empty($cachedResult)) {
6370
            return $cachedResult;
6371
        }
6372
6373
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6374
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6375
        $queryBuilder
6376
            ->select(...$columns)
6377
            ->from('pages')
6378
            ->where($queryBuilder->expr()->eq(
6379
                'uid',
6380
                $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
6381
            ));
6382
        if (!$permission->nothingIsGranted() && !$this->admin) {
6383
            $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($perms));
6384
        }
6385
        if (!$this->admin && $GLOBALS['TCA']['pages']['ctrl']['editlock'] &&
6386
            ($permission->editPagePermissionIsGranted() || $permission->deletePagePermissionIsGranted() || $permission->editContentPermissionIsGranted())
6387
        ) {
6388
            $queryBuilder->andWhere($queryBuilder->expr()->eq(
6389
                $GLOBALS['TCA']['pages']['ctrl']['editlock'],
6390
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
6391
            ));
6392
        }
6393
6394
        $row = $queryBuilder->execute()->fetch();
6395
        $this->runtimeCache->set($cacheId, $row);
6396
6397
        return $row;
6398
    }
6399
6400
    /**
6401
     * Checks if a whole branch of pages exists
6402
     *
6403
     * Tests the branch under $pid like doesRecordExist(), but it doesn't test the page with $pid as uid - use doesRecordExist() for this purpose.
6404
     * 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
6405
     *
6406
     * @param string $inList List of page uids, this is added to and returned in the end
6407
     * @param int $pid Page ID to select subpages from.
6408
     * @param int $perms Perms integer to check each page record for.
6409
     * @param bool $recurse Recursion flag: If set, it will go out through the branch.
6410
     * @return string|int List of page IDs in branch, if there are subpages, empty string if there are none or -1 if no permission
6411
     * @internal should only be used from within DataHandler
6412
     */
6413
    public function doesBranchExist($inList, $pid, $perms, $recurse)
6414
    {
6415
        $pid = (int)$pid;
6416
        $perms = (int)$perms;
6417
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6418
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6419
        $result = $queryBuilder
6420
            ->select('uid', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody')
6421
            ->from('pages')
6422
            ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
6423
            ->orderBy('sorting')
6424
            ->execute();
6425
        while ($row = $result->fetch()) {
6426
            // IF admin, then it's OK
6427
            if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $perms)) {
6428
                $inList .= $row['uid'] . ',';
6429
                if ($recurse) {
6430
                    // Follow the subpages recursively...
6431
                    $inList = $this->doesBranchExist($inList, $row['uid'], $perms, $recurse);
6432
                    if ($inList === -1) {
6433
                        return -1;
6434
                    }
6435
                }
6436
            } else {
6437
                // No permissions
6438
                return -1;
6439
            }
6440
        }
6441
        return $inList;
6442
    }
6443
6444
    /**
6445
     * Checks if the $table is readOnly
6446
     *
6447
     * @param string $table Table name
6448
     * @return bool TRUE, if readonly
6449
     * @internal should only be used from within DataHandler
6450
     */
6451
    public function tableReadOnly($table)
6452
    {
6453
        // Returns TRUE if table is readonly
6454
        return (bool)$GLOBALS['TCA'][$table]['ctrl']['readOnly'];
6455
    }
6456
6457
    /**
6458
     * Checks if the $table is only editable by admin-users
6459
     *
6460
     * @param string $table Table name
6461
     * @return bool TRUE, if readonly
6462
     * @internal should only be used from within DataHandler
6463
     */
6464
    public function tableAdminOnly($table)
6465
    {
6466
        // Returns TRUE if table is admin-only
6467
        return !empty($GLOBALS['TCA'][$table]['ctrl']['adminOnly']);
6468
    }
6469
6470
    /**
6471
     * Checks if page $id is a uid in the rootline of page id $destinationId
6472
     * Used when moving a page
6473
     *
6474
     * @param int $destinationId Destination Page ID to test
6475
     * @param int $id Page ID to test for presence inside Destination
6476
     * @return bool Returns FALSE if ID is inside destination (including equal to)
6477
     * @internal should only be used from within DataHandler
6478
     */
6479
    public function destNotInsideSelf($destinationId, $id)
6480
    {
6481
        $loopCheck = 100;
6482
        $destinationId = (int)$destinationId;
6483
        $id = (int)$id;
6484
        if ($destinationId === $id) {
6485
            return false;
6486
        }
6487
        while ($destinationId !== 0 && $loopCheck > 0) {
6488
            $loopCheck--;
6489
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6490
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6491
            $result = $queryBuilder
6492
                ->select('pid', 'uid', 't3ver_oid', 't3ver_wsid')
6493
                ->from('pages')
6494
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($destinationId, \PDO::PARAM_INT)))
6495
                ->execute();
6496
            if ($row = $result->fetch()) {
6497
                // Ensure that the moved location is used as the PID value
6498
                BackendUtility::workspaceOL('pages', $row, $this->BE_USER->workspace);
6499
                if ($row['pid'] == $id) {
6500
                    return false;
6501
                }
6502
                $destinationId = (int)$row['pid'];
6503
            } else {
6504
                return false;
6505
            }
6506
        }
6507
        return true;
6508
    }
6509
6510
    /**
6511
     * 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
6512
     * Will also generate this list for admin-users so they must be check for before calling the function
6513
     *
6514
     * @return array Array of [table]-[field] pairs to exclude from editing.
6515
     * @internal should only be used from within DataHandler
6516
     */
6517
    public function getExcludeListArray()
6518
    {
6519
        $list = [];
6520
        if (isset($this->BE_USER->groupData['non_exclude_fields'])) {
6521
            $nonExcludeFieldsArray = array_flip(GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
6522
            foreach ($GLOBALS['TCA'] as $table => $tableConfiguration) {
6523
                if (isset($tableConfiguration['columns'])) {
6524
                    foreach ($tableConfiguration['columns'] as $field => $config) {
6525
                        $isExcludeField = ($config['exclude'] ?? false);
6526
                        $isOnlyVisibleForAdmins = ($GLOBALS['TCA'][$table]['columns'][$field]['displayCond'] ?? '') === 'HIDE_FOR_NON_ADMINS';
6527
                        $editorHasPermissionForThisField = isset($nonExcludeFieldsArray[$table . ':' . $field]);
6528
                        if ($isOnlyVisibleForAdmins || ($isExcludeField && !$editorHasPermissionForThisField)) {
6529
                            $list[] = $table . '-' . $field;
6530
                        }
6531
                    }
6532
                }
6533
            }
6534
        }
6535
6536
        return $list;
6537
    }
6538
6539
    /**
6540
     * Checks if there are records on a page from tables that are not allowed
6541
     *
6542
     * @param int $page_uid Page ID
6543
     * @param int $doktype Page doktype
6544
     * @return bool|array Returns a list of the tables that are 'present' on the page but not allowed with the page_uid/doktype
6545
     * @internal should only be used from within DataHandler
6546
     */
6547
    public function doesPageHaveUnallowedTables($page_uid, $doktype)
6548
    {
6549
        $page_uid = (int)$page_uid;
6550
        if (!$page_uid) {
6551
            // Not a number. Probably a new page
6552
            return false;
6553
        }
6554
        $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6555
        // If all tables are allowed, return early
6556
        if (strpos($allowedTableList, '*') !== false) {
6557
            return false;
6558
        }
6559
        $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6560
        $tableList = [];
6561
        $allTableNames = $this->compileAdminTables();
6562
        foreach ($allTableNames as $table) {
6563
            // If the table is not in the allowed list, check if there are records...
6564
            if (in_array($table, $allowedArray, true)) {
6565
                continue;
6566
            }
6567
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6568
            $queryBuilder->getRestrictions()->removeAll();
6569
            $count = $queryBuilder
6570
                ->count('uid')
6571
                ->from($table)
6572
                ->where($queryBuilder->expr()->eq(
6573
                    'pid',
6574
                    $queryBuilder->createNamedParameter($page_uid, \PDO::PARAM_INT)
6575
                ))
6576
                ->execute()
6577
                ->fetchColumn(0);
6578
            if ($count) {
6579
                $tableList[] = $table;
6580
            }
6581
        }
6582
        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...
6583
    }
6584
6585
    /*****************************
6586
     *
6587
     * Information lookup
6588
     *
6589
     *****************************/
6590
    /**
6591
     * Returns the value of the $field from page $id
6592
     * 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!
6593
     *
6594
     * @param int $id Page uid
6595
     * @param string $field Field name for which to return value
6596
     * @return string Value of the field. Result is cached in $this->pageCache[$id][$field] and returned from there next time!
6597
     * @internal should only be used from within DataHandler
6598
     */
6599
    public function pageInfo($id, $field)
6600
    {
6601
        if (!isset($this->pageCache[$id])) {
6602
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6603
            $queryBuilder->getRestrictions()->removeAll();
6604
            $row = $queryBuilder
6605
                ->select('*')
6606
                ->from('pages')
6607
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6608
                ->execute()
6609
                ->fetch();
6610
            if ($row) {
6611
                $this->pageCache[$id] = $row;
6612
            }
6613
        }
6614
        return $this->pageCache[$id][$field];
6615
    }
6616
6617
    /**
6618
     * Returns the row of a record given by $table and $id and $fieldList (list of fields, may be '*')
6619
     * NOTICE: No check for deleted or access!
6620
     *
6621
     * @param string $table Table name
6622
     * @param int $id UID of the record from $table
6623
     * @param string $fieldList Field list for the SELECT query, eg. "*" or "uid,pid,...
6624
     * @return array|null Returns the selected record on success, otherwise NULL.
6625
     * @internal should only be used from within DataHandler
6626
     */
6627
    public function recordInfo($table, $id, $fieldList)
6628
    {
6629
        // Skip, if searching for NEW records or there's no TCA table definition
6630
        if ((int)$id === 0 || !isset($GLOBALS['TCA'][$table])) {
6631
            return null;
6632
        }
6633
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6634
        $queryBuilder->getRestrictions()->removeAll();
6635
        $result = $queryBuilder
6636
            ->select(...GeneralUtility::trimExplode(',', $fieldList))
6637
            ->from($table)
6638
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6639
            ->execute()
6640
            ->fetch();
6641
        return $result ?: null;
6642
    }
6643
6644
    /**
6645
     * Checks if record exists with and without permission check and returns that row
6646
     *
6647
     * @param string $table Record table name
6648
     * @param int $id Record UID
6649
     * @param int $perms Permission restrictions to observe: An integer that will be bitwise AND'ed.
6650
     * @param string $fieldList - fields - default is '*'
6651
     * @throws \RuntimeException
6652
     * @return array|bool Row if exists and accessible, false otherwise
6653
     */
6654
    protected function recordInfoWithPermissionCheck(string $table, int $id, int $perms, string $fieldList = '*')
6655
    {
6656
        if ($this->bypassAccessCheckForRecords) {
6657
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6658
6659
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6660
            $queryBuilder->getRestrictions()->removeAll();
6661
6662
            $record = $queryBuilder->select(...$columns)
6663
                ->from($table)
6664
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6665
                ->execute()
6666
                ->fetch();
6667
6668
            return $record ?: false;
6669
        }
6670
        if (!$perms) {
6671
            throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
6672
        }
6673
        // For all tables: Check if record exists:
6674
        $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
6675
        if (is_array($GLOBALS['TCA'][$table]) && $id > 0 && ($this->admin || $isWebMountRestrictionIgnored || $this->isRecordInWebMount($table, $id))) {
6676
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6677
            if ($table !== 'pages') {
6678
                // Find record without checking page
6679
                // @todo: This should probably check for editlock
6680
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6681
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6682
                $output = $queryBuilder
6683
                    ->select(...$columns)
6684
                    ->from($table)
6685
                    ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6686
                    ->execute()
6687
                    ->fetch();
6688
                // If record found, check page as well:
6689
                if (is_array($output)) {
6690
                    // Looking up the page for record:
6691
                    $pageRec = $this->doesRecordExist_pageLookUp($output['pid'], $perms);
6692
                    // 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):
6693
                    $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
6694
                    if (is_array($pageRec) || !$output['pid'] && ($this->admin || $isRootLevelRestrictionIgnored)) {
6695
                        return $output;
6696
                    }
6697
                }
6698
                return false;
6699
            }
6700
            return $this->doesRecordExist_pageLookUp($id, $perms, $columns);
6701
        }
6702
        return false;
6703
    }
6704
6705
    /**
6706
     * Returns an array with record properties, like header and pid
6707
     * No check for deleted or access is done!
6708
     * For versionized records, pid is resolved to its live versions pid.
6709
     * Used for logging
6710
     *
6711
     * @param string $table Table name
6712
     * @param int $id Uid of record
6713
     * @param bool $noWSOL If set, no workspace overlay is performed
6714
     * @return array Properties of record
6715
     * @internal should only be used from within DataHandler
6716
     */
6717
    public function getRecordProperties($table, $id, $noWSOL = false)
6718
    {
6719
        $row = $table === 'pages' && !$id ? ['title' => '[root-level]', 'uid' => 0, 'pid' => 0] : $this->recordInfo($table, $id, '*');
6720
        if (!$noWSOL) {
6721
            BackendUtility::workspaceOL($table, $row);
6722
        }
6723
        return $this->getRecordPropertiesFromRow($table, $row);
6724
    }
6725
6726
    /**
6727
     * Returns an array with record properties, like header and pid, based on the row
6728
     *
6729
     * @param string $table Table name
6730
     * @param array $row Input row
6731
     * @return array|null Output array
6732
     * @internal should only be used from within DataHandler
6733
     */
6734
    public function getRecordPropertiesFromRow($table, $row)
6735
    {
6736
        if ($GLOBALS['TCA'][$table]) {
6737
            $liveUid = ($row['t3ver_oid'] ?? null) ? $row['t3ver_oid'] : $row['uid'];
6738
            return [
6739
                'header' => BackendUtility::getRecordTitle($table, $row),
6740
                'pid' => $row['pid'],
6741
                'event_pid' => $this->eventPid($table, (int)$liveUid, $row['pid']),
6742
                't3ver_state' => BackendUtility::isTableWorkspaceEnabled($table) ? $row['t3ver_state'] : ''
6743
            ];
6744
        }
6745
        return null;
6746
    }
6747
6748
    /**
6749
     * @param string $table
6750
     * @param int $uid
6751
     * @param int $pid
6752
     * @return int
6753
     * @internal should only be used from within DataHandler
6754
     */
6755
    public function eventPid($table, $uid, $pid)
6756
    {
6757
        return $table === 'pages' ? $uid : $pid;
6758
    }
6759
6760
    /*********************************************
6761
     *
6762
     * Storing data to Database Layer
6763
     *
6764
     ********************************************/
6765
    /**
6766
     * Update database record
6767
     * Does not check permissions but expects them to be verified on beforehand
6768
     *
6769
     * @param string $table Record table name
6770
     * @param int $id Record uid
6771
     * @param array $fieldArray Array of field=>value pairs to insert. FIELDS MUST MATCH the database FIELDS. No check is done.
6772
     * @internal should only be used from within DataHandler
6773
     */
6774
    public function updateDB($table, $id, $fieldArray)
6775
    {
6776
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && (int)$id) {
6777
            // Do NOT update the UID field, ever!
6778
            unset($fieldArray['uid']);
6779
            if (!empty($fieldArray)) {
6780
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6781
6782
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6783
6784
                $types = [];
6785
                $platform = $connection->getDatabasePlatform();
6786
                if ($platform instanceof SQLServerPlatform) {
6787
                    // mssql needs to set proper PARAM_LOB and others to update fields
6788
                    $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
6789
                    foreach ($fieldArray as $columnName => $columnValue) {
6790
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
6791
                    }
6792
                }
6793
6794
                // Execute the UPDATE query:
6795
                $updateErrorMessage = '';
6796
                try {
6797
                    $connection->update($table, $fieldArray, ['uid' => (int)$id], $types);
6798
                } catch (DBALException $e) {
6799
                    $updateErrorMessage = $e->getPrevious()->getMessage();
6800
                }
6801
                // If succeeds, do...:
6802
                if ($updateErrorMessage === '') {
6803
                    // Update reference index:
6804
                    $this->updateRefIndex($table, $id);
6805
                    // Set History data
6806
                    $historyEntryId = 0;
6807
                    if (isset($this->historyRecords[$table . ':' . $id])) {
6808
                        $historyEntryId = $this->getRecordHistoryStore()->modifyRecord($table, $id, $this->historyRecords[$table . ':' . $id], $this->correlationId);
6809
                    }
6810
                    if ($this->enableLogging) {
6811
                        if ($this->checkStoredRecords) {
6812
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, SystemLogDatabaseAction::UPDATE);
6813
                        } else {
6814
                            $newRow = $fieldArray;
6815
                            $newRow['uid'] = $id;
6816
                        }
6817
                        // Set log entry:
6818
                        $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6819
                        $isOfflineVersion = (bool)($newRow['t3ver_oid'] ?? 0);
6820
                        $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']);
6821
                    }
6822
                    // Clear cache for relevant pages:
6823
                    $this->registerRecordIdForPageCacheClearing($table, $id);
6824
                    // Unset the pageCache for the id if table was page.
6825
                    if ($table === 'pages') {
6826
                        unset($this->pageCache[$id]);
6827
                    }
6828
                } else {
6829
                    $this->log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$updateErrorMessage, $table . ':' . $id]);
6830
                }
6831
            }
6832
        }
6833
    }
6834
6835
    /**
6836
     * Insert into database
6837
     * Does not check permissions but expects them to be verified on beforehand
6838
     *
6839
     * @param string $table Record table name
6840
     * @param string $id "NEW...." uid string
6841
     * @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!
6842
     * @param bool $newVersion Set to TRUE if new version is created.
6843
     * @param int $suggestedUid Suggested UID value for the inserted record. See the array $this->suggestedInsertUids; Admin-only feature
6844
     * @param bool $dontSetNewIdIndex If TRUE, the ->substNEWwithIDs array is not updated. Only useful in very rare circumstances!
6845
     * @return int|null Returns ID on success.
6846
     * @internal should only be used from within DataHandler
6847
     */
6848
    public function insertDB($table, $id, $fieldArray, $newVersion = false, $suggestedUid = 0, $dontSetNewIdIndex = false)
6849
    {
6850
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && isset($fieldArray['pid'])) {
6851
            // Do NOT insert the UID field, ever!
6852
            unset($fieldArray['uid']);
6853
            if (!empty($fieldArray)) {
6854
                // Check for "suggestedUid".
6855
                // This feature is used by the import functionality to force a new record to have a certain UID value.
6856
                // This is only recommended for use when the destination server is a passive mirror of another server.
6857
                // As a security measure this feature is available only for Admin Users (for now)
6858
                $suggestedUid = (int)$suggestedUid;
6859
                if ($this->BE_USER->isAdmin() && $suggestedUid && $this->suggestedInsertUids[$table . ':' . $suggestedUid]) {
6860
                    // When the value of ->suggestedInsertUids[...] is "DELETE" it will try to remove the previous record
6861
                    if ($this->suggestedInsertUids[$table . ':' . $suggestedUid] === 'DELETE') {
6862
                        $this->hardDeleteSingleRecord($table, (int)$suggestedUid);
6863
                    }
6864
                    $fieldArray['uid'] = $suggestedUid;
6865
                }
6866
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6867
                $typeArray = [];
6868
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])
6869
                    && array_key_exists($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
6870
                ) {
6871
                    $typeArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = Connection::PARAM_LOB;
6872
                }
6873
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6874
                $insertErrorMessage = '';
6875
                try {
6876
                    // Execute the INSERT query:
6877
                    $connection->insert(
6878
                        $table,
6879
                        $fieldArray,
6880
                        $typeArray
6881
                    );
6882
                } catch (DBALException $e) {
6883
                    $insertErrorMessage = $e->getPrevious()->getMessage();
6884
                }
6885
                // If succees, do...:
6886
                if ($insertErrorMessage === '') {
6887
                    // Set mapping for NEW... -> real uid:
6888
                    // the NEW_id now holds the 'NEW....' -id
6889
                    $NEW_id = $id;
6890
                    $id = $this->postProcessDatabaseInsert($connection, $table, $suggestedUid);
6891
6892
                    if (!$dontSetNewIdIndex) {
6893
                        $this->substNEWwithIDs[$NEW_id] = $id;
6894
                        $this->substNEWwithIDs_table[$NEW_id] = $table;
6895
                    }
6896
                    $newRow = [];
6897
                    if ($this->enableLogging) {
6898
                        // Checking the record is properly saved if configured
6899
                        if ($this->checkStoredRecords) {
6900
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, SystemLogDatabaseAction::INSERT);
6901
                        } else {
6902
                            $newRow = $fieldArray;
6903
                            $newRow['uid'] = $id;
6904
                        }
6905
                    }
6906
                    // Update reference index:
6907
                    $this->updateRefIndex($table, $id);
6908
6909
                    // Store in history
6910
                    $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

6910
                    $this->getRecordHistoryStore()->addRecord($table, $id, /** @scrutinizer ignore-type */ $newRow, $this->correlationId);
Loading history...
6911
6912
                    if ($newVersion) {
6913
                        if ($this->enableLogging) {
6914
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6915
                            $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);
6916
                        }
6917
                    } else {
6918
                        if ($this->enableLogging) {
6919
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6920
                            $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
6921
                            $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);
6922
                        }
6923
                        // Clear cache for relevant pages:
6924
                        $this->registerRecordIdForPageCacheClearing($table, $id);
6925
                    }
6926
                    return $id;
6927
                }
6928
                if ($this->enableLogging) {
6929
                    $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

6929
                    $this->log($table, /** @scrutinizer ignore-type */ $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
Loading history...
6930
                }
6931
            }
6932
        }
6933
        return null;
6934
    }
6935
6936
    /**
6937
     * Checking stored record to see if the written values are properly updated.
6938
     *
6939
     * @param string $table Record table name
6940
     * @param int $id Record uid
6941
     * @param array $fieldArray Array of field=>value pairs to insert/update
6942
     * @param string $action Action, for logging only.
6943
     * @return array|null Selected row
6944
     * @see insertDB()
6945
     * @see updateDB()
6946
     * @internal should only be used from within DataHandler
6947
     */
6948
    public function checkStoredRecord($table, $id, $fieldArray, $action)
6949
    {
6950
        $id = (int)$id;
6951
        if (is_array($GLOBALS['TCA'][$table]) && $id) {
6952
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6953
            $queryBuilder->getRestrictions()->removeAll();
6954
6955
            $row = $queryBuilder
6956
                ->select('*')
6957
                ->from($table)
6958
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6959
                ->execute()
6960
                ->fetch();
6961
6962
            if (!empty($row)) {
6963
                // Traverse array of values that was inserted into the database and compare with the actually stored value:
6964
                $errors = [];
6965
                foreach ($fieldArray as $key => $value) {
6966
                    if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
6967
                        if (is_float($row[$key])) {
6968
                            // if the database returns the value as double, compare it as double
6969
                            if ((double)$value !== (double)$row[$key]) {
6970
                                $errors[] = $key;
6971
                            }
6972
                        } else {
6973
                            $dbType = $GLOBALS['TCA'][$table]['columns'][$key]['config']['dbType'] ?? false;
6974
                            if ($dbType === 'datetime' || $dbType === 'time') {
6975
                                $row[$key] = $this->normalizeTimeFormat($table, $row[$key], $dbType);
6976
                            }
6977
                            if ((string)$value !== (string)$row[$key]) {
6978
                                // The is_numeric check catches cases where we want to store a float/double value
6979
                                // and database returns the field as a string with the least required amount of
6980
                                // significant digits, i.e. "0.00" being saved and "0" being read back.
6981
                                if (is_numeric($value) && is_numeric($row[$key])) {
6982
                                    if ((double)$value === (double)$row[$key]) {
6983
                                        continue;
6984
                                    }
6985
                                }
6986
                                $errors[] = $key;
6987
                            }
6988
                        }
6989
                    }
6990
                }
6991
                // Set log message if there were fields with unmatching values:
6992
                if (!empty($errors)) {
6993
                    $message = sprintf(
6994
                        '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.',
6995
                        $id,
6996
                        $table,
6997
                        implode(', ', $errors)
6998
                    );
6999
                    $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

6999
                    $this->log($table, $id, /** @scrutinizer ignore-type */ $action, 0, SystemLogErrorClassification::USER_ERROR, $message);
Loading history...
7000
                }
7001
                // Return selected rows:
7002
                return $row;
7003
            }
7004
        }
7005
        return null;
7006
    }
7007
7008
    /**
7009
     * Setting sys_history record, based on content previously set in $this->historyRecords[$table . ':' . $id] (by compareFieldArrayWithCurrentAndUnset())
7010
     *
7011
     * This functionality is now moved into the RecordHistoryStore and can be used instead.
7012
     *
7013
     * @param string $table Table name
7014
     * @param int $id Record ID
7015
     * @internal should only be used from within DataHandler
7016
     */
7017
    public function setHistory($table, $id)
7018
    {
7019
        if (isset($this->historyRecords[$table . ':' . $id])) {
7020
            $this->getRecordHistoryStore()->modifyRecord(
7021
                $table,
7022
                $id,
7023
                $this->historyRecords[$table . ':' . $id],
7024
                $this->correlationId
7025
            );
7026
        }
7027
    }
7028
7029
    /**
7030
     * @return RecordHistoryStore
7031
     */
7032
    protected function getRecordHistoryStore(): RecordHistoryStore
7033
    {
7034
        return GeneralUtility::makeInstance(
7035
            RecordHistoryStore::class,
7036
            RecordHistoryStore::USER_BACKEND,
7037
            $this->BE_USER->user['uid'],
7038
            (int)$this->BE_USER->getOriginalUserIdWhenInSwitchUserMode(),
7039
            $GLOBALS['EXEC_TIME'],
7040
            $this->BE_USER->workspace
7041
        );
7042
    }
7043
7044
    /**
7045
     * Register a table/uid combination in current user workspace for reference updating.
7046
     * Should be called on almost any update to a record which could affect references inside the record.
7047
     *
7048
     * @param string $table Table name
7049
     * @param int $uid Record UID
7050
     * @param int $workspace Workspace the record lives in
7051
     * @internal should only be used from within DataHandler
7052
     */
7053
    public function updateRefIndex($table, $uid, int $workspace = null): void
7054
    {
7055
        if ($workspace === null) {
7056
            $workspace = (int)$this->BE_USER->workspace;
7057
        }
7058
        $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)$uid, $workspace);
7059
    }
7060
7061
    /**
7062
     * Delete rows from sys_refindex a table / uid combination is involved in:
7063
     * Either on left side (tablename + recuid) OR right side (ref_table + ref_uid).
7064
     * Useful in scenarios like workspace-discard where parents or children are hard deleted: The
7065
     * expensive updateRefIndex() does not need to be called since we can just drop straight ahead.
7066
     *
7067
     * @param string $table Table name, used as tablename and ref_table
7068
     * @param int $uid Record uid, used as recuid and ref_uid
7069
     * @param int $workspace Workspace the record lives in
7070
     */
7071
    public function registerReferenceIndexRowsForDrop(string $table, int $uid, int $workspace): void
7072
    {
7073
        $this->referenceIndexUpdater->registerForDrop($table, $uid, $workspace);
7074
    }
7075
7076
    /*********************************************
7077
     *
7078
     * Misc functions
7079
     *
7080
     ********************************************/
7081
    /**
7082
     * Returning sorting number for tables with a "sortby" column
7083
     * Using when new records are created and existing records are moved around.
7084
     *
7085
     * The strategy is:
7086
     *  - if no record exists: set interval as sorting number
7087
     *  - if inserted before an element: put in the middle of the existing elements
7088
     *  - if inserted behind the last element: add interval to last sorting number
7089
     *  - if collision: move all subsequent records by 2 * interval, insert new record with collision + interval
7090
     *
7091
     * How to calculate the maximum possible inserts for the worst case of adding all records to the top,
7092
     * such that the sorting number stays within INT_MAX
7093
     *
7094
     * i = interval (currently 256)
7095
     * c = number of inserts until collision
7096
     * s = max sorting number to reach (INT_MAX - 32bit)
7097
     * n = number of records (~83 million)
7098
     *
7099
     * c = 2 * g
7100
     * g = log2(i) / 2 + 1
7101
     * n = g * s / i - g + 1
7102
     *
7103
     * The algorithm can be tuned by adjusting the interval value.
7104
     * Higher value means less collisions, but also less inserts are possible to stay within INT_MAX.
7105
     *
7106
     * @param string $table Table name
7107
     * @param int $uid Uid of record to find sorting number for. May be zero in case of new.
7108
     * @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)
7109
     * @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.
7110
     * @internal should only be used from within DataHandler
7111
     */
7112
    public function getSortNumber($table, $uid, $pid)
7113
    {
7114
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7115
        if (!$sortColumn) {
7116
            return null;
7117
        }
7118
7119
        $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($table);
7120
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
7121
        $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7122
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7123
7124
        $queryBuilder
7125
            ->select($sortColumn, 'pid', 'uid')
7126
            ->from($table);
7127
        if ($considerWorkspaces) {
7128
            $queryBuilder->addSelect('t3ver_state');
7129
        }
7130
7131
        // find and return the sorting value for the first record on that pid
7132
        if ($pid >= 0) {
7133
            // Fetches the first record (lowest sorting) under this pid
7134
            $queryBuilder
7135
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)));
7136
7137
            if ($considerWorkspaces) {
7138
                $queryBuilder->andWhere(
7139
                    $queryBuilder->expr()->orX(
7140
                        $queryBuilder->expr()->eq('t3ver_oid', 0),
7141
                        $queryBuilder->expr()->eq('t3ver_state', VersionState::MOVE_POINTER)
7142
                    )
7143
                );
7144
            }
7145
            $row = $queryBuilder
7146
                ->orderBy($sortColumn, 'ASC')
7147
                ->addOrderBy('uid', 'ASC')
7148
                ->setMaxResults(1)
7149
                ->execute()
7150
                ->fetch();
7151
7152
            if (!empty($row)) {
7153
                // The top record was the record itself, so we return its current sorting value
7154
                if ($row['uid'] == $uid) {
7155
                    return $row[$sortColumn];
7156
                }
7157
                // If the record sorting value < 1 we must resort all the records under this pid
7158
                if ($row[$sortColumn] < 1) {
7159
                    $this->increaseSortingOfFollowingRecords($table, (int)$pid);
7160
                    // Lowest sorting value after full resorting is $sortIntervals
7161
                    return $this->sortIntervals;
7162
                }
7163
                // Sorting number between current top element and zero
7164
                return floor($row[$sortColumn] / 2);
7165
            }
7166
            // No records, so we choose the default value as sorting-number
7167
            return $this->sortIntervals;
7168
        }
7169
7170
        // Find and return first possible sorting value AFTER record with given uid ($pid)
7171
        // Fetches the record which is supposed to be the prev record
7172
        $row = $queryBuilder
7173
                ->where($queryBuilder->expr()->eq(
7174
                    'uid',
7175
                    $queryBuilder->createNamedParameter(abs($pid), \PDO::PARAM_INT)
7176
                ))
7177
                ->execute()
7178
                ->fetch();
7179
7180
        // There is a previous record
7181
        if (!empty($row)) {
7182
            // Look if the record UID happens to be a versioned record. If so, find its live version.
7183
            // If this is already a moved record in workspace, this is not needed
7184
            if ((int)$row['t3ver_state'] !== VersionState::MOVE_POINTER && $lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortColumn . ',pid,uid')) {
7185
                $row = $lookForLiveVersion;
7186
            } elseif ($considerWorkspaces && $this->BE_USER->workspace > 0) {
7187
                // In case the previous record is moved in the workspace, we need to fetch the information from this specific record
7188
                $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $row['uid'], $sortColumn . ',pid,uid,t3ver_state');
7189
                if (is_array($versionedRecord) && (int)$versionedRecord['t3ver_state'] === VersionState::MOVE_POINTER) {
7190
                    $row = $versionedRecord;
7191
                }
7192
            }
7193
            // If the record should be inserted after itself, keep the current sorting information:
7194
            if ((int)$row['uid'] === (int)$uid) {
7195
                $sortNumber = $row[$sortColumn];
7196
            } else {
7197
                $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7198
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7199
7200
                $queryBuilder
7201
                        ->select($sortColumn, 'pid', 'uid')
7202
                        ->from($table)
7203
                        ->where(
7204
                            $queryBuilder->expr()->eq(
7205
                                'pid',
7206
                                $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT)
7207
                            ),
7208
                            $queryBuilder->expr()->gte(
7209
                                $sortColumn,
7210
                                $queryBuilder->createNamedParameter($row[$sortColumn], \PDO::PARAM_INT)
7211
                            )
7212
                        )
7213
                        ->orderBy($sortColumn, 'ASC')
7214
                        ->addOrderBy('uid', 'DESC')
7215
                        ->setMaxResults(2);
7216
7217
                if ($considerWorkspaces) {
7218
                    $queryBuilder->andWhere(
7219
                        $queryBuilder->expr()->orX(
7220
                            $queryBuilder->expr()->eq('t3ver_oid', 0),
7221
                            $queryBuilder->expr()->eq('t3ver_state', VersionState::MOVE_POINTER)
7222
                        )
7223
                    );
7224
                }
7225
7226
                $subResults = $queryBuilder
7227
                    ->execute()
7228
                    ->fetchAll();
7229
                // Fetches the next record in order to calculate the in-between sortNumber
7230
                // There was a record afterwards
7231
                if (count($subResults) === 2) {
7232
                    // There was a record afterwards, fetch that
7233
                    $subrow = array_pop($subResults);
7234
                    // The sortNumber is found in between these values
7235
                    $sortNumber = $row[$sortColumn] + floor(($subrow[$sortColumn] - $row[$sortColumn]) / 2);
7236
                    // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
7237
                    if ($sortNumber <= $row[$sortColumn] || $sortNumber >= $subrow[$sortColumn]) {
7238
                        $this->increaseSortingOfFollowingRecords($table, (int)$row['pid'], (int)$row[$sortColumn]);
7239
                        $sortNumber = $row[$sortColumn] + $this->sortIntervals;
7240
                    }
7241
                } else {
7242
                    // If after the last record in the list, we just add the sortInterval to the last sortvalue
7243
                    $sortNumber = $row[$sortColumn] + $this->sortIntervals;
7244
                }
7245
            }
7246
            return ['pid' => $row['pid'], 'sortNumber' => $sortNumber];
7247
        }
7248
        if ($this->enableLogging) {
7249
            $propArr = $this->getRecordProperties($table, $uid);
7250
            // OK, don't insert $propArr['event_pid'] here...
7251
            $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']);
7252
        }
7253
        // There MUST be a previous record or else this cannot work
7254
        return false;
7255
    }
7256
7257
    /**
7258
     * Increases sorting field value of all records with sorting higher than $sortingValue
7259
     *
7260
     * Used internally by getSortNumber() to "make space" in sorting values when inserting new record
7261
     *
7262
     * @param string $table Table name
7263
     * @param int $pid Page Uid in which to resort records
7264
     * @param int $sortingValue All sorting numbers larger than this number will be shifted
7265
     * @see getSortNumber()
7266
     */
7267
    protected function increaseSortingOfFollowingRecords(string $table, int $pid, int $sortingValue = null): void
7268
    {
7269
        $sortBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7270
        if ($sortBy) {
7271
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7272
7273
            $queryBuilder
7274
                ->update($table)
7275
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7276
                ->set($sortBy, $queryBuilder->quoteIdentifier($sortBy) . ' + ' . $this->sortIntervals . ' + ' . $this->sortIntervals, false);
7277
            if ($sortingValue !== null) {
7278
                $queryBuilder->andWhere($queryBuilder->expr()->gt($sortBy, $sortingValue));
7279
            }
7280
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
7281
                $queryBuilder
7282
                    ->andWhere(
7283
                        $queryBuilder->expr()->eq('t3ver_oid', 0)
7284
                    );
7285
            }
7286
7287
            $deleteColumn = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? '';
7288
            if ($deleteColumn) {
7289
                $queryBuilder->andWhere($queryBuilder->expr()->eq($deleteColumn, 0));
7290
            }
7291
7292
            $queryBuilder->execute();
7293
        }
7294
    }
7295
7296
    /**
7297
     * Returning uid of previous localized record, if any, for tables with a "sortby" column
7298
     * Used when new localized records are created so that localized records are sorted in the same order as the default language records
7299
     *
7300
     * For a given record (A) uid (record we're translating) it finds first default language record (from the same colpos)
7301
     * with sorting smaller than given record (B).
7302
     * Then it fetches a translated version of record B and returns it's uid.
7303
     *
7304
     * If there is no record B, or it has no translation in given language, the record A uid is returned.
7305
     * The localized record will be placed the after record which uid is returned.
7306
     *
7307
     * @param string $table Table name
7308
     * @param int $uid Uid of default language record
7309
     * @param int $pid Pid of default language record
7310
     * @param int $language Language of localization
7311
     * @return int uid of record after which the localized record should be inserted
7312
     */
7313
    protected function getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
7314
    {
7315
        $previousLocalizedRecordUid = $uid;
7316
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7317
        if ($sortColumn) {
7318
            $select = [$sortColumn, 'pid', 'uid'];
7319
            // For content elements, we also need the colPos
7320
            if ($table === 'tt_content') {
7321
                $select[] = 'colPos';
7322
            }
7323
            // Get the sort value of the default language record
7324
            $row = BackendUtility::getRecord($table, $uid, implode(',', $select));
7325
            if (is_array($row)) {
7326
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7327
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7328
7329
                $queryBuilder
7330
                    ->select(...$select)
7331
                    ->from($table)
7332
                    ->where(
7333
                        $queryBuilder->expr()->eq(
7334
                            'pid',
7335
                            $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
7336
                        ),
7337
                        $queryBuilder->expr()->eq(
7338
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
7339
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
7340
                        ),
7341
                        $queryBuilder->expr()->lt(
7342
                            $sortColumn,
7343
                            $queryBuilder->createNamedParameter($row[$sortColumn], \PDO::PARAM_INT)
7344
                        )
7345
                    )
7346
                    ->orderBy($sortColumn, 'DESC')
7347
                    ->addOrderBy('uid', 'DESC')
7348
                    ->setMaxResults(1);
7349
                if ($table === 'tt_content') {
7350
                    $queryBuilder
7351
                        ->andWhere(
7352
                            $queryBuilder->expr()->eq(
7353
                                'colPos',
7354
                                $queryBuilder->createNamedParameter($row['colPos'], \PDO::PARAM_INT)
7355
                            )
7356
                        );
7357
                }
7358
                // If there is an element, find its localized record in specified localization language
7359
                if ($previousRow = $queryBuilder->execute()->fetch()) {
7360
                    $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $language);
7361
                    if (is_array($previousLocalizedRecord[0])) {
7362
                        $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
7363
                    }
7364
                }
7365
            }
7366
        }
7367
        return $previousLocalizedRecordUid;
7368
    }
7369
7370
    /**
7371
     * 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.
7372
     * Used for new records and during copy operations for defaults
7373
     *
7374
     * @param string $table Table name for which to set default values.
7375
     * @return array Array with default values.
7376
     * @internal should only be used from within DataHandler
7377
     */
7378
    public function newFieldArray($table)
7379
    {
7380
        $fieldArray = [];
7381
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
7382
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $content) {
7383
                if (isset($this->defaultValues[$table][$field])) {
7384
                    $fieldArray[$field] = $this->defaultValues[$table][$field];
7385
                } elseif (isset($content['config']['default'])) {
7386
                    $fieldArray[$field] = $content['config']['default'];
7387
                }
7388
            }
7389
        }
7390
        return $fieldArray;
7391
    }
7392
7393
    /**
7394
     * 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.
7395
     *
7396
     * @param string $table Table name
7397
     * @param array $incomingFieldArray Incoming array (passed by reference)
7398
     * @internal should only be used from within DataHandler
7399
     */
7400
    public function addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
7401
    {
7402
        // Checking languages:
7403
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
7404
            if (!isset($incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
7405
                // Language field must be found in input row - otherwise it does not make sense.
7406
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
7407
                    ->getQueryBuilderForTable('sys_language');
7408
                $queryBuilder->getRestrictions()
7409
                    ->removeAll()
7410
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7411
                $queryBuilder
7412
                    ->select('uid')
7413
                    ->from('sys_language')
7414
                    ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
7415
                $rows = array_merge([['uid' => 0]], $queryBuilder->execute()->fetchAll(), [['uid' => -1]]);
7416
                foreach ($rows as $r) {
7417
                    if ($this->BE_USER->checkLanguageAccess($r['uid'])) {
7418
                        $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $r['uid'];
7419
                        break;
7420
                    }
7421
                }
7422
            }
7423
        }
7424
    }
7425
7426
    /**
7427
     * Returns the $data array from $table overridden in the fields defined in ->overrideValues.
7428
     *
7429
     * @param string $table Table name
7430
     * @param array $data Data array with fields from table. These will be overlaid with values in $this->overrideValues[$table]
7431
     * @return array Data array, processed.
7432
     * @internal should only be used from within DataHandler
7433
     */
7434
    public function overrideFieldArray($table, $data)
7435
    {
7436
        if (is_array($this->overrideValues[$table])) {
7437
            $data = array_merge($data, $this->overrideValues[$table]);
7438
        }
7439
        return $data;
7440
    }
7441
7442
    /**
7443
     * Compares the incoming field array with the current record and unsets all fields which are the same.
7444
     * Used for existing records being updated
7445
     *
7446
     * @param string $table Record table name
7447
     * @param int $id Record uid
7448
     * @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!
7449
     * @return array Returns $fieldArray. If the returned array is empty, then the record should not be updated!
7450
     * @internal should only be used from within DataHandler
7451
     */
7452
    public function compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
7453
    {
7454
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7455
        $queryBuilder = $connection->createQueryBuilder();
7456
        $queryBuilder->getRestrictions()->removeAll();
7457
        $currentRecord = $queryBuilder->select('*')
7458
            ->from($table)
7459
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7460
            ->execute()
7461
            ->fetch();
7462
        // If the current record exists (which it should...), begin comparison:
7463
        if (is_array($currentRecord)) {
7464
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
7465
            $columnRecordTypes = [];
7466
            foreach ($currentRecord as $columnName => $_) {
7467
                $columnRecordTypes[$columnName] = '';
7468
                $type = $tableDetails->getColumn($columnName)->getType();
7469
                if ($type instanceof IntegerType) {
7470
                    $columnRecordTypes[$columnName] = 'int';
7471
                }
7472
            }
7473
            // Unset the fields which are similar:
7474
            foreach ($fieldArray as $col => $val) {
7475
                $fieldConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
7476
                $isNullField = (!empty($fieldConfiguration['eval']) && GeneralUtility::inList($fieldConfiguration['eval'], 'null'));
7477
7478
                // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
7479
                // In general this avoids to store superfluous data which also will be visualized in the editing history.
7480
                if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $columnRecordTypes[$col], $isNullField)) {
7481
                    unset($fieldArray[$col]);
7482
                } else {
7483
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
7484
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
7485
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
7486
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
7487
                    }
7488
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
7489
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
7490
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
7491
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
7492
                    }
7493
                }
7494
            }
7495
        } else {
7496
            // If the current record does not exist this is an error anyways and we just return an empty array here.
7497
            $fieldArray = [];
7498
        }
7499
        return $fieldArray;
7500
    }
7501
7502
    /**
7503
     * Determines whether submitted values and stored values are equal.
7504
     * This prevents from adding superfluous field changes which would be shown in the record history as well.
7505
     * For NULL fields (see accordant TCA definition 'eval' = 'null'), a special handling is required since
7506
     * (!strcmp(NULL, '')) would be a false-positive.
7507
     *
7508
     * @param mixed $submittedValue Value that has submitted (e.g. from a backend form)
7509
     * @param mixed $storedValue Value that is currently stored in the database
7510
     * @param string $storedType SQL type of the stored value column (see mysql_field_type(), e.g 'int', 'string',  ...)
7511
     * @param bool $allowNull Whether NULL values are allowed by accordant TCA definition ('eval' = 'null')
7512
     * @return bool Whether both values are considered to be equal
7513
     */
7514
    protected function isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
7515
    {
7516
        // No NULL values are allowed, this is the regular behaviour.
7517
        // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
7518
        if (!$allowNull) {
7519
            $result = (string)$submittedValue === (string)$storedValue || $storedType === 'int' && (int)$storedValue === (int)$submittedValue;
7520
        // Null values are allowed, but currently there's a real (not NULL) value.
7521
        // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
7522
        } elseif ($storedValue !== null) {
7523
            $result = (
7524
                $submittedValue !== null
7525
                && $this->isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
7526
            );
7527
        // Null values are allowed, and currently there's a NULL value.
7528
        // Thus, check whether a NULL value was submitted.
7529
        } else {
7530
            $result = ($submittedValue === null);
7531
        }
7532
7533
        return $result;
7534
    }
7535
7536
    /**
7537
     * Converts a HTML entity (like &#123;) to the character '123'
7538
     *
7539
     * @param string $input Input string
7540
     * @return string Output string
7541
     * @internal should only be used from within DataHandler
7542
     */
7543
    public function convNumEntityToByteValue($input)
7544
    {
7545
        $token = md5(microtime());
7546
        $parts = explode($token, preg_replace('/(&#([0-9]+);)/', $token . '\\2' . $token, $input));
7547
        foreach ($parts as $k => $v) {
7548
            if ($k % 2) {
7549
                $v = (int)$v;
7550
                // Just to make sure that control bytes are not converted.
7551
                if ($v > 32) {
7552
                    $parts[$k] = chr($v);
7553
                }
7554
            }
7555
        }
7556
        return implode('', $parts);
7557
    }
7558
7559
    /**
7560
     * Disables the delete clause for fetching records.
7561
     * In general only undeleted records will be used. If the delete
7562
     * clause is disabled, also deleted records are taken into account.
7563
     */
7564
    public function disableDeleteClause()
7565
    {
7566
        $this->disableDeleteClause = true;
7567
    }
7568
7569
    /**
7570
     * Returns delete-clause for the $table
7571
     *
7572
     * @param string $table Table name
7573
     * @return string Delete clause
7574
     * @internal should only be used from within DataHandler
7575
     */
7576
    public function deleteClause($table)
7577
    {
7578
        // Returns the proper delete-clause if any for a table from TCA
7579
        if (!$this->disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
7580
            return ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
7581
        }
7582
        return '';
7583
    }
7584
7585
    /**
7586
     * Add delete restriction if not disabled
7587
     *
7588
     * @param QueryRestrictionContainerInterface $restrictions
7589
     */
7590
    protected function addDeleteRestriction(QueryRestrictionContainerInterface $restrictions)
7591
    {
7592
        if (!$this->disableDeleteClause) {
7593
            $restrictions->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7594
        }
7595
    }
7596
7597
    /**
7598
     * Gets UID of parent record. If record is deleted it will be looked up in
7599
     * an array built before the record was deleted
7600
     *
7601
     * @param string $table Table where record lives/lived
7602
     * @param int $uid Record UID
7603
     * @return int[] Parent UIDs
7604
     */
7605
    protected function getOriginalParentOfRecord($table, $uid)
7606
    {
7607
        if (isset(self::$recordPidsForDeletedRecords[$table][$uid])) {
7608
            return self::$recordPidsForDeletedRecords[$table][$uid];
7609
        }
7610
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
7611
        return [$parentUid];
7612
    }
7613
7614
    /**
7615
     * Extract entries from TSconfig for a specific table. This will merge specific and default configuration together.
7616
     *
7617
     * @param string $table Table name
7618
     * @param array $TSconfig TSconfig for page
7619
     * @return array TSconfig merged
7620
     * @internal should only be used from within DataHandler
7621
     */
7622
    public function getTableEntries($table, $TSconfig)
7623
    {
7624
        $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : [];
7625
        $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : [];
7626
        ArrayUtility::mergeRecursiveWithOverrule($dA, $tA);
7627
        return $dA;
7628
    }
7629
7630
    /**
7631
     * Returns the pid of a record from $table with $uid
7632
     *
7633
     * @param string $table Table name
7634
     * @param int $uid Record uid
7635
     * @return int|false PID value (unless the record did not exist in which case FALSE is returned)
7636
     * @internal should only be used from within DataHandler
7637
     */
7638
    public function getPID($table, $uid)
7639
    {
7640
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7641
        $queryBuilder->getRestrictions()
7642
            ->removeAll();
7643
        $queryBuilder->select('pid')
7644
            ->from($table)
7645
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
7646
        if ($row = $queryBuilder->execute()->fetch()) {
7647
            return $row['pid'];
7648
        }
7649
        return false;
7650
    }
7651
7652
    /**
7653
     * Executing dbAnalysisStore
7654
     * This will save MM relations for new records but is executed after records are created because we need to know the ID of them
7655
     * @internal should only be used from within DataHandler
7656
     */
7657
    public function dbAnalysisStoreExec()
7658
    {
7659
        foreach ($this->dbAnalysisStore as $action) {
7660
            $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
7661
            if ($id) {
7662
                $action[0]->writeMM($action[1], $id, $action[3]);
7663
            }
7664
        }
7665
    }
7666
7667
    /**
7668
     * Returns array, $CPtable, of pages under the $pid going down to $counter levels.
7669
     * Selecting ONLY pages which the user has read-access to!
7670
     *
7671
     * @param array $CPtable Accumulation of page uid=>pid pairs in branch of $pid
7672
     * @param int $pid Page ID for which to find subpages
7673
     * @param int $counter Number of levels to go down.
7674
     * @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!
7675
     * @return array Return array.
7676
     * @internal should only be used from within DataHandler
7677
     */
7678
    public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
7679
    {
7680
        if ($counter) {
7681
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
7682
            $restrictions = $queryBuilder->getRestrictions()->removeAll();
7683
            $this->addDeleteRestriction($restrictions);
7684
            $queryBuilder
7685
                ->select('uid')
7686
                ->from('pages')
7687
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7688
                ->orderBy('sorting', 'DESC');
7689
            if (!$this->admin) {
7690
                $queryBuilder->andWhere($this->BE_USER->getPagePermsClause(Permission::PAGE_SHOW));
7691
            }
7692
            if ((int)$this->BE_USER->workspace === 0) {
7693
                $queryBuilder->andWhere(
7694
                    $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
7695
                );
7696
            } else {
7697
                $queryBuilder->andWhere($queryBuilder->expr()->in(
7698
                    't3ver_wsid',
7699
                    $queryBuilder->createNamedParameter([0, $this->BE_USER->workspace], Connection::PARAM_INT_ARRAY)
7700
                ));
7701
            }
7702
            $result = $queryBuilder->execute();
7703
7704
            $pages = [];
7705
            while ($row = $result->fetch()) {
7706
                $pages[$row['uid']] = $row;
7707
            }
7708
7709
            // Resolve placeholders of workspace versions
7710
            if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
7711
                $pages = array_reverse(
7712
                    $this->resolveVersionedRecords(
7713
                        'pages',
7714
                        'uid',
7715
                        'sorting',
7716
                        array_keys($pages)
7717
                    ),
7718
                    true
7719
                );
7720
            }
7721
7722
            foreach ($pages as $page) {
7723
                if ($page['uid'] != $rootID) {
7724
                    $CPtable[$page['uid']] = $pid;
7725
                    // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
7726
                    if ($counter - 1) {
7727
                        $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
7728
                    }
7729
                }
7730
            }
7731
        }
7732
        return $CPtable;
7733
    }
7734
7735
    /**
7736
     * List of all tables (those administrators has access to = array_keys of $GLOBALS['TCA'])
7737
     *
7738
     * @return array Array of all TCA table names
7739
     * @internal should only be used from within DataHandler
7740
     */
7741
    public function compileAdminTables()
7742
    {
7743
        return array_keys($GLOBALS['TCA']);
7744
    }
7745
7746
    /**
7747
     * Checks if any uniqueInPid eval input fields are in the record and if so, they are re-written to be correct.
7748
     *
7749
     * @param string $table Table name
7750
     * @param int $uid Record UID
7751
     * @internal should only be used from within DataHandler
7752
     */
7753
    public function fixUniqueInPid($table, $uid)
7754
    {
7755
        if (empty($GLOBALS['TCA'][$table])) {
7756
            return;
7757
        }
7758
7759
        $curData = $this->recordInfo($table, $uid, '*');
7760
        $newData = [];
7761
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7762
            if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
7763
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7764
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
7765
                    $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
7766
                    if ((string)$newV !== (string)$curData[$field]) {
7767
                        $newData[$field] = $newV;
7768
                    }
7769
                }
7770
            }
7771
        }
7772
        // IF there are changed fields, then update the database
7773
        if (!empty($newData)) {
7774
            $this->updateDB($table, $uid, $newData);
7775
        }
7776
    }
7777
7778
    /**
7779
     * Checks if any uniqueInSite eval fields are in the record and if so, they are re-written to be correct.
7780
     *
7781
     * @param string $table Table name
7782
     * @param int $uid Record UID
7783
     * @return bool whether the record had to be fixed or not
7784
     */
7785
    protected function fixUniqueInSite(string $table, int $uid): bool
7786
    {
7787
        $curData = $this->recordInfo($table, $uid, '*');
7788
        $workspaceId = $this->BE_USER->workspace;
7789
        $newData = [];
7790
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7791
            if ($conf['config']['type'] === 'slug' && (string)$curData[$field] !== '') {
7792
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7793
                if (in_array('uniqueInSite', $evalCodesArray, true)) {
7794
                    $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $conf['config'], $workspaceId);
7795
                    $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

7795
                    $state = RecordStateFactory::forName($table)->fromArray(/** @scrutinizer ignore-type */ $curData);
Loading history...
7796
                    $newValue = $helper->buildSlugForUniqueInSite($curData[$field], $state);
7797
                    if ((string)$newValue !== (string)$curData[$field]) {
7798
                        $newData[$field] = $newValue;
7799
                    }
7800
                }
7801
            }
7802
        }
7803
        // IF there are changed fields, then update the database
7804
        if (!empty($newData)) {
7805
            $this->updateDB($table, $uid, $newData);
7806
            return true;
7807
        }
7808
        return false;
7809
    }
7810
7811
    /**
7812
     * Check if there are subpages that need an adoption as well
7813
     * @param int $pageId
7814
     */
7815
    protected function fixUniqueInSiteForSubpages(int $pageId)
7816
    {
7817
        // Get ALL subpages to update - read-permissions are respected
7818
        $subPages = $this->int_pageTreeInfo([], $pageId, 99, $pageId);
7819
        // Now fix uniqueInSite for subpages
7820
        foreach ($subPages as $thePageUid => $thePagePid) {
7821
            $recordWasModified = $this->fixUniqueInSite('pages', $thePageUid);
7822
            if ($recordWasModified) {
7823
                // @todo: Add logging and history - but how? we don't know the data that was in the system before
7824
            }
7825
        }
7826
    }
7827
7828
    /**
7829
     * When er record is copied you can specify fields from the previous record which should be copied into the new one
7830
     * 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)
7831
     *
7832
     * @param string $table Table name
7833
     * @param int $uid Record UID
7834
     * @param int $prevUid UID of previous record
7835
     * @param bool $update If set, updates the record
7836
     * @param array $newData Input array. If fields are already specified AND $update is not set, values are not set in output array.
7837
     * @return array Output array (For when the copying operation needs to get the information instead of updating the info)
7838
     * @internal should only be used from within DataHandler
7839
     */
7840
    public function fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData = [])
7841
    {
7842
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
7843
            $prevData = $this->recordInfo($table, $prevUid, '*');
7844
            $theFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
7845
            foreach ($theFields as $field) {
7846
                if ($GLOBALS['TCA'][$table]['columns'][$field] && ($update || !isset($newData[$field]))) {
7847
                    $newData[$field] = $prevData[$field];
7848
                }
7849
            }
7850
            if ($update && !empty($newData)) {
7851
                $this->updateDB($table, $uid, $newData);
7852
            }
7853
        }
7854
        return $newData;
7855
    }
7856
7857
    /**
7858
     * Casts a reference value. In case MM relations or foreign_field
7859
     * references are used. All other configurations, as well as
7860
     * foreign_table(!) could be stored as comma-separated-values
7861
     * as well. Since the system is not able to determine the default
7862
     * value automatically then, the TCA default value is used if
7863
     * it has been defined.
7864
     *
7865
     * @param int|string $value The value to be casted (e.g. '', '0', '1,2,3')
7866
     * @param array $configuration The TCA configuration of the accordant field
7867
     * @return int|string
7868
     */
7869
    protected function castReferenceValue($value, array $configuration)
7870
    {
7871
        if ((string)$value !== '') {
7872
            return $value;
7873
        }
7874
7875
        if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
7876
            return 0;
7877
        }
7878
7879
        if (array_key_exists('default', $configuration)) {
7880
            return $configuration['default'];
7881
        }
7882
7883
        return $value;
7884
    }
7885
7886
    /**
7887
     * Returns TRUE if the TCA/columns field type is a DB reference field
7888
     *
7889
     * @param array $conf Config array for TCA/columns field
7890
     * @return bool TRUE if DB reference field (group/db or select with foreign-table)
7891
     * @internal should only be used from within DataHandler
7892
     */
7893
    public function isReferenceField($conf)
7894
    {
7895
        return $conf['type'] === 'group' && $conf['internal_type'] === 'db' || $conf['type'] === 'select' && $conf['foreign_table'];
7896
    }
7897
7898
    /**
7899
     * Returns the subtype as a string of an inline field.
7900
     * If it's not an inline field at all, it returns FALSE.
7901
     *
7902
     * @param array $conf Config array for TCA/columns field
7903
     * @return string|bool string Inline subtype (field|mm|list), boolean: FALSE
7904
     * @internal should only be used from within DataHandler
7905
     */
7906
    public function getInlineFieldType($conf)
7907
    {
7908
        if ($conf['type'] !== 'inline' || !$conf['foreign_table']) {
7909
            return false;
7910
        }
7911
        if ($conf['foreign_field']) {
7912
            // The reference to the parent is stored in a pointer field in the child record
7913
            return 'field';
7914
        }
7915
        if ($conf['MM']) {
7916
            // Regular MM intermediate table is used to store data
7917
            return 'mm';
7918
        }
7919
        // An item list (separated by comma) is stored (like select type is doing)
7920
        return 'list';
7921
    }
7922
7923
    /**
7924
     * Get modified header for a copied record
7925
     *
7926
     * @param string $table Table name
7927
     * @param int $pid PID value in which other records to test might be
7928
     * @param string $field Field name to get header value for.
7929
     * @param string $value Current field value
7930
     * @param int $count Counter (number of recursions)
7931
     * @param string $prevTitle Previous title we checked for (in previous recursion)
7932
     * @return string The field value, possibly appended with a "copy label
7933
     * @internal should only be used from within DataHandler
7934
     */
7935
    public function getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
7936
    {
7937
        // Set title value to check for:
7938
        $checkTitle = $value;
7939
        if ($count > 0) {
7940
            $checkTitle = $value . rtrim(' ' . sprintf($this->prependLabel($table), $count));
7941
        }
7942
        // Do check:
7943
        if ($prevTitle != $checkTitle || $count < 100) {
7944
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7945
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7946
            $rowCount = $queryBuilder
7947
                ->count('uid')
7948
                ->from($table)
7949
                ->where(
7950
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
7951
                    $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($checkTitle, \PDO::PARAM_STR))
7952
                )
7953
                ->execute()
7954
                ->fetchColumn(0);
7955
            if ($rowCount) {
7956
                return $this->getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
7957
            }
7958
        }
7959
        // Default is to just return the current input title if no other was returned before:
7960
        return $checkTitle;
7961
    }
7962
7963
    /**
7964
     * Return "copy" label for a table. Although the name is "prepend" it actually APPENDs the label (after ...)
7965
     *
7966
     * @param string $table Table name
7967
     * @return string Label to append, containing "%s" for the number
7968
     * @see getCopyHeader()
7969
     * @internal should only be used from within DataHandler
7970
     */
7971
    public function prependLabel($table)
7972
    {
7973
        return $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
7974
    }
7975
7976
    /**
7977
     * Get the final pid based on $table and $pid ($destPid type... pos/neg)
7978
     *
7979
     * @param string $table Table name
7980
     * @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!
7981
     * @return int
7982
     * @internal should only be used from within DataHandler
7983
     */
7984
    public function resolvePid($table, $pid)
7985
    {
7986
        $pid = (int)$pid;
7987
        if ($pid < 0) {
7988
            $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7989
            $query->getRestrictions()
7990
                ->removeAll();
7991
            $row = $query
7992
                ->select('pid')
7993
                ->from($table)
7994
                ->where($query->expr()->eq('uid', $query->createNamedParameter(abs($pid), \PDO::PARAM_INT)))
7995
                ->execute()
7996
                ->fetch();
7997
            $pid = (int)$row['pid'];
7998
        }
7999
        return $pid;
8000
    }
8001
8002
    /**
8003
     * Removes the prependAtCopy prefix on values
8004
     *
8005
     * @param string $table Table name
8006
     * @param string $value The value to fix
8007
     * @return string Clean name
8008
     * @internal should only be used from within DataHandler
8009
     */
8010
    public function clearPrefixFromValue($table, $value)
8011
    {
8012
        $regex = '/\s' . sprintf(preg_quote($this->prependLabel($table)), '[0-9]*') . '$/';
8013
        return @preg_replace($regex, '', $value);
8014
    }
8015
8016
    /**
8017
     * Check if there are records from tables on the pages to be deleted which the current user is not allowed to
8018
     *
8019
     * @param int[] $pageIds IDs of pages which should be checked
8020
     * @return string[]|null Return null, if permission granted, otherwise an array with the tables that are not allowed to be deleted
8021
     * @see canDeletePage()
8022
     */
8023
    protected function checkForRecordsFromDisallowedTables(array $pageIds): ?array
8024
    {
8025
        if ($this->admin) {
8026
            return null;
8027
        }
8028
8029
        $disallowedTables = [];
8030
        if (!empty($pageIds)) {
8031
            $tableNames = $this->compileAdminTables();
8032
            foreach ($tableNames as $table) {
8033
                $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8034
                $query->getRestrictions()
8035
                    ->removeAll()
8036
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8037
                $count = $query->count('uid')
8038
                    ->from($table)
8039
                    ->where($query->expr()->in(
8040
                        'pid',
8041
                        $query->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
8042
                    ))
8043
                    ->execute()
8044
                    ->fetchColumn(0);
8045
                if ($count && ($this->tableReadOnly($table) || !$this->checkModifyAccessList($table))) {
8046
                    $disallowedTables[] = $table;
8047
                }
8048
            }
8049
        }
8050
        return !empty($disallowedTables) ? $disallowedTables : null;
8051
    }
8052
8053
    /**
8054
     * Determine if a record was copied or if a record is the result of a copy action.
8055
     *
8056
     * @param string $table The tablename of the record
8057
     * @param int $uid The uid of the record
8058
     * @return bool Returns TRUE if the record is copied or is the result of a copy action
8059
     * @internal should only be used from within DataHandler
8060
     */
8061
    public function isRecordCopied($table, $uid)
8062
    {
8063
        // If the record was copied:
8064
        if (isset($this->copyMappingArray[$table][$uid])) {
8065
            return true;
8066
        }
8067
        if (isset($this->copyMappingArray[$table]) && in_array($uid, array_values($this->copyMappingArray[$table]))) {
8068
            return true;
8069
        }
8070
        return false;
8071
    }
8072
8073
    /******************************
8074
     *
8075
     * Clearing cache
8076
     *
8077
     ******************************/
8078
8079
    /**
8080
     * Clearing the cache based on a page being updated
8081
     * If the $table is 'pages' then cache is cleared for all pages on the same level (and subsequent?)
8082
     * Else just clear the cache for the parent page of the record.
8083
     *
8084
     * @param string $table Table name of record that was just updated.
8085
     * @param int $uid UID of updated / inserted record
8086
     * @param int $pid REAL PID of page of a deleted/moved record to get TSconfig in ClearCache.
8087
     * @internal This method is not meant to be called directly but only from the core itself or from hooks
8088
     */
8089
    public function registerRecordIdForPageCacheClearing($table, $uid, $pid = null)
8090
    {
8091
        if (!is_array(static::$recordsToClearCacheFor[$table])) {
8092
            static::$recordsToClearCacheFor[$table] = [];
8093
        }
8094
        static::$recordsToClearCacheFor[$table][] = (int)$uid;
8095
        if ($pid !== null) {
8096
            if (!is_array(static::$recordPidsForDeletedRecords[$table])) {
8097
                static::$recordPidsForDeletedRecords[$table] = [];
8098
            }
8099
            static::$recordPidsForDeletedRecords[$table][$uid][] = (int)$pid;
8100
        }
8101
    }
8102
8103
    /**
8104
     * Do the actual clear cache
8105
     */
8106
    protected function processClearCacheQueue()
8107
    {
8108
        $tagsToClear = [];
8109
        $clearCacheCommands = [];
8110
8111
        foreach (static::$recordsToClearCacheFor as $table => $uids) {
8112
            foreach (array_unique($uids) as $uid) {
8113
                if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
8114
                    return;
8115
                }
8116
                // For move commands we may get more then 1 parent.
8117
                $pageUids = $this->getOriginalParentOfRecord($table, $uid);
8118
                foreach ($pageUids as $originalParent) {
8119
                    [$tagsToClearFromPrepare, $clearCacheCommandsFromPrepare]
8120
                        = $this->prepareCacheFlush($table, $uid, $originalParent);
8121
                    $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
8122
                    $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
8123
                }
8124
            }
8125
        }
8126
8127
        /** @var CacheManager $cacheManager */
8128
        $cacheManager = $this->getCacheManager();
8129
        $cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear));
8130
8131
        // Filter duplicate cache commands from cacheQueue
8132
        $clearCacheCommands = array_unique($clearCacheCommands);
8133
        // Execute collected clear cache commands from page TSConfig
8134
        foreach ($clearCacheCommands as $command) {
8135
            $this->clear_cacheCmd($command);
8136
        }
8137
8138
        // Reset the cache clearing array
8139
        static::$recordsToClearCacheFor = [];
8140
8141
        // Reset the original pid array
8142
        static::$recordPidsForDeletedRecords = [];
8143
    }
8144
8145
    /**
8146
     * Prepare the cache clearing
8147
     *
8148
     * @param string $table Table name of record that needs to be cleared
8149
     * @param int $uid UID of record for which the cache needs to be cleared
8150
     * @param int $pid Original pid of the page of the record which the cache needs to be cleared
8151
     * @return array Array with tagsToClear and clearCacheCommands
8152
     * @internal This function is internal only it may be changed/removed also in minor version numbers.
8153
     */
8154
    protected function prepareCacheFlush($table, $uid, $pid)
8155
    {
8156
        $tagsToClear = [];
8157
        $clearCacheCommands = [];
8158
        $pageUid = 0;
8159
        // Get Page TSconfig relevant:
8160
        $TSConfig = BackendUtility::getPagesTSconfig($pid)['TCEMAIN.'] ?? [];
8161
        if (empty($TSConfig['clearCache_disable']) && $this->BE_USER->workspace === 0) {
8162
            $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
8163
            // If table is "pages":
8164
            $pageIdsThatNeedCacheFlush = [];
8165
            if ($table === 'pages') {
8166
                // Find out if the record is a get the original page
8167
                $pageUid = $this->getDefaultLanguagePageId($uid);
8168
8169
                // Builds list of pages on the SAME level as this page (siblings)
8170
                $queryBuilder = $connectionPool->getQueryBuilderForTable('pages');
8171
                $queryBuilder->getRestrictions()
8172
                    ->removeAll()
8173
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8174
                $siblings = $queryBuilder
8175
                    ->select('A.pid AS pid', 'B.uid AS uid')
8176
                    ->from('pages', 'A')
8177
                    ->from('pages', 'B')
8178
                    ->where(
8179
                        $queryBuilder->expr()->eq('A.uid', $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)),
8180
                        $queryBuilder->expr()->eq('B.pid', $queryBuilder->quoteIdentifier('A.pid')),
8181
                        $queryBuilder->expr()->gte('A.pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8182
                    )
8183
                    ->execute();
8184
8185
                $parentPageId = 0;
8186
                while ($row_tmp = $siblings->fetch()) {
8187
                    $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
8188
                    $parentPageId = (int)$row_tmp['pid'];
8189
                    // Add children as well:
8190
                    if ($TSConfig['clearCache_pageSiblingChildren']) {
8191
                        $siblingChildrenQuery = $connectionPool->getQueryBuilderForTable('pages');
8192
                        $siblingChildrenQuery->getRestrictions()
8193
                            ->removeAll()
8194
                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8195
                        $siblingChildren = $siblingChildrenQuery
8196
                            ->select('uid')
8197
                            ->from('pages')
8198
                            ->where($siblingChildrenQuery->expr()->eq(
8199
                                'pid',
8200
                                $siblingChildrenQuery->createNamedParameter($row_tmp['uid'], \PDO::PARAM_INT)
8201
                            ))
8202
                            ->execute();
8203
                        while ($row_tmp2 = $siblingChildren->fetch()) {
8204
                            $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
8205
                        }
8206
                    }
8207
                }
8208
                // Finally, add the parent page as well when clearing a specific page
8209
                if ($parentPageId > 0) {
8210
                    $pageIdsThatNeedCacheFlush[] = $parentPageId;
8211
                }
8212
                // Add grand-parent as well if configured
8213
                if ($TSConfig['clearCache_pageGrandParent']) {
8214
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8215
                    $parentQuery->getRestrictions()
8216
                        ->removeAll()
8217
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8218
                    $row_tmp = $parentQuery
8219
                        ->select('pid')
8220
                        ->from('pages')
8221
                        ->where($parentQuery->expr()->eq(
8222
                            'uid',
8223
                            $parentQuery->createNamedParameter($parentPageId, \PDO::PARAM_INT)
8224
                        ))
8225
                        ->execute()
8226
                        ->fetch();
8227
                    if (!empty($row_tmp)) {
8228
                        $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
8229
                    }
8230
                }
8231
            } else {
8232
                // For other tables than "pages", delete cache for the records "parent page".
8233
                $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
8234
                // Add the parent page as well
8235
                if ($TSConfig['clearCache_pageGrandParent']) {
8236
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8237
                    $parentQuery->getRestrictions()
8238
                        ->removeAll()
8239
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8240
                    $parentPageRecord = $parentQuery
8241
                        ->select('pid')
8242
                        ->from('pages')
8243
                        ->where($parentQuery->expr()->eq(
8244
                            'uid',
8245
                            $parentQuery->createNamedParameter($pageUid, \PDO::PARAM_INT)
8246
                        ))
8247
                        ->execute()
8248
                        ->fetch();
8249
                    if (!empty($parentPageRecord)) {
8250
                        $pageIdsThatNeedCacheFlush[] = (int)$parentPageRecord['pid'];
8251
                    }
8252
                }
8253
            }
8254
            // Call pre-processing function for clearing of cache for page ids:
8255
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8256
                $_params = ['pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()'];
8257
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8258
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8259
            }
8260
            // Delete cache for selected pages:
8261
            foreach ($pageIdsThatNeedCacheFlush as $pageId) {
8262
                $tagsToClear['pageId_' . $pageId] = true;
8263
            }
8264
            // Queue delete cache for current table and record
8265
            $tagsToClear[$table] = true;
8266
            $tagsToClear[$table . '_' . $uid] = true;
8267
        }
8268
        // Clear cache for pages entered in TSconfig:
8269
        if (!empty($TSConfig['clearCacheCmd'])) {
8270
            $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
8271
            $clearCacheCommands = array_unique($commands);
8272
        }
8273
        // Call post processing function for clear-cache:
8274
        $_params = ['table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig, 'tags' => $tagsToClear];
8275
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8276
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8277
        }
8278
        return [
8279
            $tagsToClear,
8280
            $clearCacheCommands
8281
        ];
8282
    }
8283
8284
    /**
8285
     * Clears the cache based on the command $cacheCmd.
8286
     *
8287
     * $cacheCmd='pages'
8288
     * Clears cache for all pages and page-based caches inside the cache manager.
8289
     * Requires admin-flag to be set for BE_USER.
8290
     *
8291
     * $cacheCmd='all'
8292
     * Clears all cache_tables. This is necessary if templates are updated.
8293
     * Requires admin-flag to be set for BE_USER.
8294
     *
8295
     * The following cache_* are intentionally not cleared by 'all'
8296
     *
8297
     * - imagesizes:	Clearing this table would cause a lot of unneeded
8298
     * Imagemagick calls because the size information has
8299
     * to be fetched again after clearing.
8300
     * - all caches inside the cache manager that are inside the group "system"
8301
     * - they are only needed to build up the core system and templates.
8302
     *   If the group of system caches needs to be deleted explicitly, use
8303
     *   flushCachesInGroup('system') of CacheManager directly.
8304
     *
8305
     * $cacheCmd=[integer]
8306
     * Clears cache for the page pointed to by $cacheCmd (an integer).
8307
     *
8308
     * $cacheCmd='cacheTag:[string]'
8309
     * Flush page and pagesection cache by given tag
8310
     *
8311
     * $cacheCmd='cacheId:[string]'
8312
     * Removes cache identifier from page and page section cache
8313
     *
8314
     * Can call a list of post processing functions as defined in
8315
     * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc']
8316
     * (numeric array with values being the function references, called by
8317
     * GeneralUtility::callUserFunction()).
8318
     *
8319
     *
8320
     * @param string $cacheCmd The cache command, see above description
8321
     */
8322
    public function clear_cacheCmd($cacheCmd)
8323
    {
8324
        if (is_object($this->BE_USER)) {
8325
            $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]);
8326
        }
8327
        $userTsConfig = $this->BE_USER->getTSConfig();
8328
        switch (strtolower($cacheCmd)) {
8329
            case 'pages':
8330
                if ($this->admin || ($userTsConfig['options.']['clearCache.']['pages'] ?? false)) {
8331
                    $this->getCacheManager()->flushCachesInGroup('pages');
8332
                }
8333
                break;
8334
            case 'all':
8335
                // allow to clear all caches if the TS config option is enabled or the option is not explicitly
8336
                // disabled for admins (which could clear all caches by default). The latter option is useful
8337
                // for big production sites where it should be possible to restrict the cache clearing for some admins.
8338
                if (($userTsConfig['options.']['clearCache.']['all'] ?? false)
8339
                    || ($this->admin && (bool)($userTsConfig['options.']['clearCache.']['all'] ?? true))
8340
                ) {
8341
                    $this->getCacheManager()->flushCaches();
8342
                    GeneralUtility::makeInstance(ConnectionPool::class)
8343
                        ->getConnectionForTable('cache_treelist')
8344
                        ->truncate('cache_treelist');
8345
8346
                    // Delete Opcode Cache
8347
                    GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
8348
                }
8349
                break;
8350
        }
8351
8352
        $tagsToFlush = [];
8353
        // Clear cache for a page ID!
8354
        if (MathUtility::canBeInterpretedAsInteger($cacheCmd)) {
8355
            $list_cache = [$cacheCmd];
8356
            // Call pre-processing function for clearing of cache for page ids:
8357
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8358
                $_params = ['pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()'];
8359
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8360
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8361
            }
8362
            // Delete cache for selected pages:
8363
            if (is_array($list_cache)) {
0 ignored issues
show
introduced by
The condition is_array($list_cache) is always true.
Loading history...
8364
                foreach ($list_cache as $pageId) {
8365
                    $tagsToFlush[] = 'pageId_' . (int)$pageId;
8366
                }
8367
            }
8368
        }
8369
        // flush cache by tag
8370
        if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
8371
            $cacheTag = substr($cacheCmd, 9);
8372
            $tagsToFlush[] = $cacheTag;
8373
        }
8374
        // process caching framework operations
8375
        if (!empty($tagsToFlush)) {
8376
            $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

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