Completed
Push — master ( 787850...28ebf5 )
by
unknown
41:28 queued 27:40
created

DataHandler::deleteClause()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
671
        }
672
673
        // set correlation id for each new set of data or commands
674
        $this->correlationId = CorrelationId::forScope(
675
            md5(StringUtility::getUniqueId(self::class))
676
        );
677
678
        // Get default values from user TSconfig
679
        $tcaDefaultOverride = $this->BE_USER->getTSConfig()['TCAdefaults.'] ?? null;
680
        if (is_array($tcaDefaultOverride)) {
681
            $this->setDefaultsFromUserTS($tcaDefaultOverride);
682
        }
683
684
        // Initializing default permissions for pages
685
        $defaultPermissions = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'];
686
        if (isset($defaultPermissions['user'])) {
687
            $this->defaultPermissions['user'] = $defaultPermissions['user'];
688
        }
689
        if (isset($defaultPermissions['group'])) {
690
            $this->defaultPermissions['group'] = $defaultPermissions['group'];
691
        }
692
        if (isset($defaultPermissions['everybody'])) {
693
            $this->defaultPermissions['everybody'] = $defaultPermissions['everybody'];
694
        }
695
        // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
696
        if (!$this->admin) {
697
            $this->excludedTablesAndFields = array_flip($this->getExcludeListArray());
698
        }
699
        // Setting the data and cmd arrays
700
        if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
701
            reset($data);
702
            $this->datamap = $data;
703
        }
704
        if (is_array($cmd)) {
0 ignored issues
show
introduced by
The condition is_array($cmd) is always true.
Loading history...
705
            reset($cmd);
706
            $this->cmdmap = $cmd;
707
        }
708
    }
709
710
    /**
711
     * Function that can mirror input values in datamap-array to other uid numbers.
712
     * 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]
713
     *
714
     * @param array $mirror This array has the syntax $mirror[table_name][uid] = [list of uids to copy data-value TO!]
715
     */
716
    public function setMirror($mirror)
717
    {
718
        if (!is_array($mirror)) {
0 ignored issues
show
introduced by
The condition is_array($mirror) is always true.
Loading history...
719
            return;
720
        }
721
722
        foreach ($mirror as $table => $uid_array) {
723
            if (!isset($this->datamap[$table])) {
724
                continue;
725
            }
726
727
            foreach ($uid_array as $id => $uidList) {
728
                if (!isset($this->datamap[$table][$id])) {
729
                    continue;
730
                }
731
732
                $theIdsInArray = GeneralUtility::trimExplode(',', $uidList, true);
733
                foreach ($theIdsInArray as $copyToUid) {
734
                    $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
735
                }
736
            }
737
        }
738
    }
739
740
    /**
741
     * Initializes default values coming from User TSconfig
742
     *
743
     * @param array $userTS User TSconfig array
744
     */
745
    public function setDefaultsFromUserTS($userTS)
746
    {
747
        if (!is_array($userTS)) {
0 ignored issues
show
introduced by
The condition is_array($userTS) is always true.
Loading history...
748
            return;
749
        }
750
751
        foreach ($userTS as $k => $v) {
752
            $k = mb_substr($k, 0, -1);
753
            if (!$k || !is_array($v) || !isset($GLOBALS['TCA'][$k])) {
754
                continue;
755
            }
756
757
            if (is_array($this->defaultValues[$k])) {
758
                $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
759
            } else {
760
                $this->defaultValues[$k] = $v;
761
            }
762
        }
763
    }
764
765
    /**
766
     * Dummy method formerly used for file handling.
767
     *
768
     * @deprecated since TYPO3 v10.0, will be removed in TYPO3 v11.0.
769
     */
770
    public function process_uploads()
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::process_uploads" is not in camel caps format
Loading history...
771
    {
772
        trigger_error('DataHandler->process_uploads() will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
773
    }
774
775
    /*********************************************
776
     *
777
     * HOOKS
778
     *
779
     *********************************************/
780
    /**
781
     * Hook: processDatamap_afterDatabaseOperations
782
     * (calls $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);)
783
     *
784
     * Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
785
     * but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
786
     *
787
     * @param array $hookObjectsArr (reference) Array with hook objects
788
     * @param string $status (reference) Status of the current operation, 'new' or 'update
789
     * @param string $table (reference) The table currently processing data for
790
     * @param string $id (reference) The record uid currently processing data for, [integer] or [string] (like 'NEW...')
791
     * @param array $fieldArray (reference) The field array of a record
792
     */
793
    public function hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::hook_processDatamap_afterDatabaseOperations" is not in camel caps format
Loading history...
794
    {
795
        // Process hook directly:
796
        if (!isset($this->remapStackRecords[$table][$id])) {
797
            foreach ($hookObjectsArr as $hookObj) {
798
                if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
799
                    $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
800
                }
801
            }
802
        } else {
803
            $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = [
804
                'status' => $status,
805
                'fieldArray' => $fieldArray,
806
                'hookObjectsArr' => $hookObjectsArr
807
            ];
808
        }
809
    }
810
811
    /**
812
     * Gets the 'checkModifyAccessList' hook objects.
813
     * The first call initializes the accordant objects.
814
     *
815
     * @return array The 'checkModifyAccessList' hook objects (if any)
816
     * @throws \UnexpectedValueException
817
     */
818
    protected function getCheckModifyAccessListHookObjects()
819
    {
820
        if (!isset($this->checkModifyAccessListHookObjects)) {
821
            $this->checkModifyAccessListHookObjects = [];
822
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] ?? [] as $className) {
823
                $hookObject = GeneralUtility::makeInstance($className);
824
                if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
825
                    throw new \UnexpectedValueException($className . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
826
                }
827
                $this->checkModifyAccessListHookObjects[] = $hookObject;
828
            }
829
        }
830
        return $this->checkModifyAccessListHookObjects;
831
    }
832
833
    /*********************************************
834
     *
835
     * PROCESSING DATA
836
     *
837
     *********************************************/
838
    /**
839
     * Processing the data-array
840
     * Call this function to process the data-array set by start()
841
     *
842
     * @return bool|void
843
     */
844
    public function process_datamap()
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::process_datamap" is not in camel caps format
Loading history...
845
    {
846
        $this->controlActiveElements();
847
848
        // Keep versionized(!) relations here locally:
849
        $registerDBList = [];
850
        $this->registerElementsToBeDeleted();
851
        $this->datamap = $this->unsetElementsToBeDeleted($this->datamap);
852
        // Editing frozen:
853
        if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
854
            $this->newlog('All editing in this workspace has been frozen!', SystemLogErrorClassification::USER_ERROR);
855
            return false;
856
        }
857
        // First prepare user defined objects (if any) for hooks which extend this function:
858
        $hookObjectsArr = [];
859
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] ?? [] as $className) {
860
            $hookObject = GeneralUtility::makeInstance($className);
861
            if (method_exists($hookObject, 'processDatamap_beforeStart')) {
862
                $hookObject->processDatamap_beforeStart($this);
863
            }
864
            $hookObjectsArr[] = $hookObject;
865
        }
866
        // Pre-process data-map and synchronize localization states
867
        $this->datamap = GeneralUtility::makeInstance(SlugEnricher::class)->enrichDataMap($this->datamap);
868
        $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER)->process();
869
        // 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.
870
        $orderOfTables = [];
871
        // Set pages first.
872
        if (isset($this->datamap['pages'])) {
873
            $orderOfTables[] = 'pages';
874
        }
875
        $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
876
        // Process the tables...
877
        foreach ($orderOfTables as $table) {
878
            // Check if
879
            //	   - table is set in $GLOBALS['TCA'],
880
            //	   - table is NOT readOnly
881
            //	   - the table is set with content in the data-array (if not, there's nothing to process...)
882
            //	   - permissions for tableaccess OK
883
            $modifyAccessList = $this->checkModifyAccessList($table);
884
            if (!$modifyAccessList) {
885
                $this->log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
886
            }
887
            if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
888
                continue;
889
            }
890
891
            if ($this->reverseOrder) {
892
                $this->datamap[$table] = array_reverse($this->datamap[$table], 1);
893
            }
894
            // For each record from the table, do:
895
            // $id is the record uid, may be a string if new records...
896
            // $incomingFieldArray is the array of fields
897
            foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
898
                if (!is_array($incomingFieldArray)) {
899
                    continue;
900
                }
901
                $theRealPid = null;
902
903
                // Hook: processDatamap_preProcessFieldArray
904
                foreach ($hookObjectsArr as $hookObj) {
905
                    if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
906
                        $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
907
                    }
908
                }
909
                // ******************************
910
                // Checking access to the record
911
                // ******************************
912
                $createNewVersion = false;
913
                $recordAccess = false;
914
                $old_pid_value = '';
915
                // Is it a new record? (Then Id is a string)
916
                if (!MathUtility::canBeInterpretedAsInteger($id)) {
917
                    // Get a fieldArray with tca default values
918
                    $fieldArray = $this->newFieldArray($table);
919
                    // A pid must be set for new records.
920
                    if (isset($incomingFieldArray['pid'])) {
921
                        $pid_value = $incomingFieldArray['pid'];
922
                        // Checking and finding numerical pid, it may be a string-reference to another value
923
                        $canProceed = true;
924
                        // If a NEW... id
925
                        if (strpos($pid_value, 'NEW') !== false) {
926
                            if ($pid_value[0] === '-') {
927
                                $negFlag = -1;
928
                                $pid_value = substr($pid_value, 1);
929
                            } else {
930
                                $negFlag = 1;
931
                            }
932
                            // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
933
                            if (isset($this->substNEWwithIDs[$pid_value])) {
934
                                if ($negFlag === 1) {
935
                                    $old_pid_value = $this->substNEWwithIDs[$pid_value];
936
                                }
937
                                $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
938
                            } else {
939
                                $canProceed = false;
940
                            }
941
                        }
942
                        $pid_value = (int)$pid_value;
943
                        if ($canProceed) {
944
                            $fieldArray = $this->resolveSortingAndPidForNewRecord($table, $pid_value, $fieldArray);
945
                        }
946
                    }
947
                    $theRealPid = $fieldArray['pid'];
948
                    // Now, check if we may insert records on this pid.
949
                    if ($theRealPid >= 0) {
950
                        // Checks if records can be inserted on this $pid.
951
                        // If this is a page translation, the check needs to be done for the l10n_parent record
952
                        if ($table === 'pages' && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0 && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0) {
953
                            $recordAccess = $this->checkRecordInsertAccess($table, $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]);
954
                        } else {
955
                            $recordAccess = $this->checkRecordInsertAccess($table, $theRealPid);
956
                        }
957
                        if ($recordAccess) {
958
                            $this->addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray);
959
                            $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
960
                            if (!$recordAccess) {
961
                                $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
962
                            } elseif (!$this->bypassWorkspaceRestrictions) {
963
                                // Workspace related processing:
964
                                // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
965
                                if (!$this->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
966
                                    if (BackendUtility::isTableWorkspaceEnabled($table)) {
967
                                        $createNewVersion = true;
968
                                    } else {
969
                                        $recordAccess = false;
970
                                        $this->newlog('Record could not be created in this workspace in this branch', SystemLogErrorClassification::USER_ERROR);
971
                                    }
972
                                }
973
                            }
974
                        }
975
                    } else {
976
                        $this->logger->debug('Internal ERROR: pid should not be less than zero!');
977
                    }
978
                    // Yes new record, change $record_status to 'insert'
979
                    $status = 'new';
980
                } else {
981
                    // Nope... $id is a number
982
                    $fieldArray = [];
983
                    $recordAccess = $this->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $hookObjectsArr);
984
                    if (!$recordAccess) {
985
                        if ($this->enableLogging) {
986
                            $propArr = $this->getRecordProperties($table, $id);
987
                            $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']);
988
                        }
989
                        continue;
990
                    }
991
                    // Next check of the record permissions (internals)
992
                    $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
993
                    if (!$recordAccess) {
994
                        $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
995
                    } else {
996
                        // Here we fetch the PID of the record that we point to...
997
                        $tempdata = $this->recordInfo($table, $id, 'pid' . (BackendUtility::isTableWorkspaceEnabled($table) ? ',t3ver_oid,t3ver_wsid,t3ver_stage' : ''));
998
                        $theRealPid = $tempdata['pid'] ?? null;
999
                        // Use the new id of the versionized record we're trying to write to:
1000
                        // (This record is a child record of a parent and has already been versionized.)
1001
                        if (!empty($this->autoVersionIdMap[$table][$id])) {
1002
                            // For the reason that creating a new version of this record, automatically
1003
                            // created related child records (e.g. "IRRE"), update the accordant field:
1004
                            $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1005
                            // Use the new id of the copied/versionized record:
1006
                            $id = $this->autoVersionIdMap[$table][$id];
1007
                            $recordAccess = true;
1008
                        } elseif (!$this->bypassWorkspaceRestrictions && ($errorCode = $this->BE_USER->workspaceCannotEditRecord($table, $tempdata))) {
1009
                            $recordAccess = false;
1010
                            // Versioning is required and it must be offline version!
1011
                            // Check if there already is a workspace version
1012
                            $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
1013
                            if ($workspaceVersion) {
1014
                                $id = $workspaceVersion['uid'];
1015
                                $recordAccess = true;
1016
                            } elseif ($this->BE_USER->workspaceAllowAutoCreation($table, $id, $theRealPid)) {
1017
                                // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1018
                                $this->pagetreeNeedsRefresh = true;
1019
1020
                                /** @var DataHandler $tce */
1021
                                $tce = GeneralUtility::makeInstance(__CLASS__);
1022
                                $tce->enableLogging = $this->enableLogging;
1023
                                // Setting up command for creating a new version of the record:
1024
                                $cmd = [];
1025
                                $cmd[$table][$id]['version'] = [
1026
                                    'action' => 'new',
1027
                                    // Default is to create a version of the individual records
1028
                                    'label' => 'Auto-created for WS #' . $this->BE_USER->workspace
1029
                                ];
1030
                                $tce->start([], $cmd, $this->BE_USER);
1031
                                $tce->process_cmdmap();
1032
                                $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
1033
                                // If copying was successful, share the new uids (also of related children):
1034
                                if (!empty($tce->copyMappingArray[$table][$id])) {
1035
                                    foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
1036
                                        foreach ($origIdArray as $origId => $newId) {
1037
                                            $this->autoVersionIdMap[$origTable][$origId] = $newId;
1038
                                        }
1039
                                    }
1040
                                    // Update registerDBList, that holds the copied relations to child records:
1041
                                    $registerDBList = array_merge($registerDBList, $tce->registerDBList);
1042
                                    // For the reason that creating a new version of this record, automatically
1043
                                    // created related child records (e.g. "IRRE"), update the accordant field:
1044
                                    $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1045
                                    // Use the new id of the copied/versionized record:
1046
                                    $id = $this->autoVersionIdMap[$table][$id];
1047
                                    $recordAccess = true;
1048
                                } else {
1049
                                    $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);
1050
                                }
1051
                            } else {
1052
                                $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);
1053
                            }
1054
                        }
1055
                    }
1056
                    // The default is 'update'
1057
                    $status = 'update';
1058
                }
1059
                // If access was granted above, proceed to create or update record:
1060
                if (!$recordAccess) {
1061
                    continue;
1062
                }
1063
1064
                // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
1065
                [$tscPID] = BackendUtility::getTSCpid($table, $id, $old_pid_value ?: $fieldArray['pid']);
1066
                if ($status === 'new' && $table === 'pages') {
1067
                    $fieldArray = $this->pagePermissionAssembler->applyDefaults(
1068
                        $fieldArray,
1069
                        (int)$tscPID,
1070
                        (int)$this->userid,
1071
                        (int)$this->BE_USER->firstMainGroup
1072
                    );
1073
                }
1074
                // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1075
                $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1076
                $newVersion_placeholderFieldArray = [];
1077
                if ($createNewVersion) {
1078
                    // create a placeholder array with already processed field content
1079
                    $newVersion_placeholderFieldArray = $fieldArray;
1080
                }
1081
                // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations to field!
1082
                // Forcing some values unto field array:
1083
                // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1084
                $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1085
                if ($createNewVersion) {
1086
                    $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1087
                }
1088
                // Setting system fields
1089
                if ($status === 'new') {
1090
                    if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1091
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1092
                        if ($createNewVersion) {
1093
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1094
                        }
1095
                    }
1096
                    if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1097
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1098
                        if ($createNewVersion) {
1099
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1100
                        }
1101
                    }
1102
                } elseif ($this->checkSimilar) {
1103
                    // Removing fields which are equal to the current value:
1104
                    $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1105
                }
1106
                if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1107
                    $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1108
                    if ($createNewVersion) {
1109
                        $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1110
                    }
1111
                }
1112
                // Set stage to "Editing" to make sure we restart the workflow
1113
                if (BackendUtility::isTableWorkspaceEnabled($table)) {
1114
                    $fieldArray['t3ver_stage'] = 0;
1115
                }
1116
                // Hook: processDatamap_postProcessFieldArray
1117
                foreach ($hookObjectsArr as $hookObj) {
1118
                    if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1119
                        $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1120
                    }
1121
                }
1122
                // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1123
                // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already
1124
                if (is_array($fieldArray)) {
1125
                    if ($status === 'new') {
1126
                        if ($table === 'pages') {
1127
                            // for new pages always a refresh is needed
1128
                            $this->pagetreeNeedsRefresh = true;
1129
                        }
1130
1131
                        // This creates a new version of the record with online placeholder and offline version
1132
                        if ($createNewVersion) {
1133
                            // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1134
                            $this->pagetreeNeedsRefresh = true;
1135
1136
                            // Setting placeholder state value for temporary record
1137
                            $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1138
                            // Setting workspace - only so display of placeholders can filter out those from other workspaces.
1139
                            $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1140
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $this->getPlaceholderTitleForTableLabel($table);
1141
                            // Saving placeholder as 'original'
1142
                            $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1143
                            // For the actual new offline version, set versioning values to point to placeholder:
1144
                            $fieldArray['pid'] = -1;
1145
                            $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1146
                            // Setting placeholder state value for version (so it can know it is currently a new version...)
1147
                            $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1148
                            $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1149
                            // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1150
                            $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1151
                            if ($phShadowId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $phShadowId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

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

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

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

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

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

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

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

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

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

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

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

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

2794
                $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, /** @scrutinizer ignore-type */ $prep);
Loading history...
2795
                if ($oldRelations != $newRelations) {
2796
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
2797
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
2798
                } else {
2799
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
2800
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
2801
                }
2802
            } else {
2803
                $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
2804
            }
2805
            $valueArray = $dbAnalysis->countItems();
2806
        } else {
2807
            $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

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

3224
        BackendUtility::workspaceOL($table, /** @scrutinizer ignore-type */ $row, -99, false);
Loading history...
3225
        $row = BackendUtility::purgeComputedPropertiesFromRecord($row);
3226
3227
        // Initializing:
3228
        $theNewID = StringUtility::getUniqueId('NEW');
3229
        $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3230
        $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3231
        // Getting "copy-after" fields if applicable:
3232
        $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

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

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

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

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

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

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

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

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

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

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

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

4078
                [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4079
                $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4080
                // Setting PID
4081
                $updateFields['pid'] = $destPid;
4082
                // Table is sorted by 'sortby'
4083
                if ($sortColumn && !isset($updateFields[$sortColumn])) {
4084
                    $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4085
                    $updateFields[$sortColumn] = $sortNumber;
4086
                }
4087
                // Check for child records that have also to be moved
4088
                $this->moveRecord_procFields($table, $uid, $destPid);
4089
                // Create query for update:
4090
                GeneralUtility::makeInstance(ConnectionPool::class)
4091
                    ->getConnectionForTable($table)
4092
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4093
                // Check for the localizations of that element
4094
                $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4095
                // Call post processing hooks:
4096
                foreach ($hookObjectsArr as $hookObj) {
4097
                    if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4098
                        $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4099
                    }
4100
                }
4101
4102
                $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4103
                if ($this->enableLogging) {
4104
                    // Logging...
4105
                    $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4106
                    if ($destPid != $propArr['pid']) {
4107
                        // Logged to old page
4108
                        $newPropArr = $this->getRecordProperties($table, $uid);
4109
                        $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4110
                        $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']);
4111
                        // Logged to new page
4112
                        $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);
4113
                    } else {
4114
                        // Logged to new page
4115
                        $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);
4116
                    }
4117
                }
4118
                // Clear cache after moving
4119
                $this->registerRecordIdForPageCacheClearing($table, $uid);
4120
                $this->fixUniqueInPid($table, $uid);
4121
                $this->fixUniqueInSite($table, (int)$uid);
4122
                if ($table === 'pages') {
4123
                    $this->fixUniqueInSiteForSubpages((int)$uid);
4124
                }
4125
                // fixCopyAfterDuplFields
4126
                if ($origDestPid < 0) {
4127
                    $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
0 ignored issues
show
Bug introduced by
It seems like abs($origDestPid) 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

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

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

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

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

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

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

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

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

4772
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4773
        $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4774
        $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
4775
        $databaseErrorMessage = '';
4776
        if ($deleteField && !$forceHardDelete) {
4777
            $updateFields = [
4778
                $deleteField => $undeleteRecord ? 0 : 1
4779
            ];
4780
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4781
                $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4782
            }
4783
            // before (un-)deleting this record, check for child records or references
4784
            $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
4785
            try {
4786
                GeneralUtility::makeInstance(ConnectionPool::class)
4787
                    ->getConnectionForTable($table)
4788
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4789
                // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
4790
                if (!$undeleteRecord) {
4791
                    $this->deletedRecords[$table][] = (int)$uid;
4792
                    $this->deleteL10nOverlayRecords($table, $uid);
4793
                }
4794
            } catch (DBALException $e) {
4795
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4796
            }
4797
        } else {
4798
            // Delete the hard way...:
4799
            try {
4800
                GeneralUtility::makeInstance(ConnectionPool::class)
4801
                    ->getConnectionForTable($table)
4802
                    ->delete($table, ['uid' => (int)$uid]);
4803
                $this->deletedRecords[$table][] = (int)$uid;
4804
                $this->deleteL10nOverlayRecords($table, $uid);
4805
            } catch (DBALException $e) {
4806
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4807
            }
4808
        }
4809
        if ($this->enableLogging) {
4810
            $state = $undeleteRecord ? SystemLogDatabaseAction::INSERT : SystemLogDatabaseAction::DELETE;
4811
            if ($databaseErrorMessage === '') {
4812
                if ($forceHardDelete) {
4813
                    $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
4814
                } else {
4815
                    $message = $state === 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
4816
                }
4817
                $propArr = $this->getRecordProperties($table, $uid);
4818
                $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4819
4820
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::MESSAGE, $message, 0, [
4821
                    $propArr['header'],
4822
                    $table . ':' . $uid,
4823
                    $pagePropArr['header'],
4824
                    $propArr['pid']
4825
                ], $propArr['event_pid']);
4826
            } else {
4827
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::TODAYS_SPECIAL, $databaseErrorMessage);
4828
            }
4829
        }
4830
4831
        // Add history entry
4832
        if ($undeleteRecord) {
4833
            $this->getRecordHistoryStore()->undeleteRecord($table, $uid, $this->correlationId);
4834
        } else {
4835
            $this->getRecordHistoryStore()->deleteRecord($table, $uid, $this->correlationId);
4836
        }
4837
4838
        // Update reference index:
4839
        $this->updateRefIndex($table, $uid);
4840
4841
        // We track calls to update the reference index as to avoid calling it twice
4842
        // with the same arguments. This is done because reference indexing is quite
4843
        // costly and the update reference index stack usually contain duplicates.
4844
        // NB: also filled and checked in loop below. The initialisation prevents
4845
        // running the "root" record twice if it appears in the stack twice.
4846
        $updateReferenceIndexCalls = [[$table, $uid]];
4847
4848
        // If there are entries in the updateRefIndexStack
4849
        if (is_array($this->updateRefIndexStack[$table]) && is_array($this->updateRefIndexStack[$table][$uid])) {
4850
            while ($args = array_pop($this->updateRefIndexStack[$table][$uid])) {
4851
                if (!in_array($args, $updateReferenceIndexCalls, true)) {
4852
                    // $args[0]: table, $args[1]: uid
4853
                    $this->updateRefIndex($args[0], $args[1]);
4854
                    $updateReferenceIndexCalls[] = $args;
4855
                }
4856
            }
4857
            unset($this->updateRefIndexStack[$table][$uid]);
4858
        }
4859
    }
4860
4861
    /**
4862
     * Used to delete page because it will check for branch below pages and disallowed tables on the page as well.
4863
     *
4864
     * @param int $uid Page id
4865
     * @param bool $force If TRUE, pages are not checked for permission.
4866
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4867
     */
4868
    public function deletePages($uid, $force = false, $forceHardDelete = false)
4869
    {
4870
        $uid = (int)$uid;
4871
        if ($uid === 0) {
4872
            if ($this->enableLogging) {
4873
                $this->log('pages', $uid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'Deleting all pages starting from the root-page is disabled.', -1, [], 0);
4874
            }
4875
            return;
4876
        }
4877
        // Getting list of pages to delete:
4878
        if ($force) {
4879
            // Returns the branch WITHOUT permission checks (0 secures that), so it cannot return -1
4880
            $pageIdsInBranch = $this->doesBranchExist('', $uid, 0, true);
4881
            $res = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
4882
        } else {
4883
            $res = $this->canDeletePage($uid);
4884
        }
4885
        // Perform deletion if not error:
4886
        if (is_array($res)) {
4887
            foreach ($res as $deleteId) {
4888
                $this->deleteSpecificPage($deleteId, $forceHardDelete);
4889
            }
4890
        } else {
4891
            /** @var FlashMessage $flashMessage */
4892
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
4893
            /** @var FlashMessageService $flashMessageService */
4894
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
4895
            $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
4896
            $this->newlog($res, SystemLogErrorClassification::USER_ERROR);
4897
        }
4898
    }
4899
4900
    /**
4901
     * Delete a page and all records on it.
4902
     *
4903
     * @param int $uid Page id
4904
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4905
     * @internal
4906
     * @see deletePages()
4907
     */
4908
    public function deleteSpecificPage($uid, $forceHardDelete = false)
4909
    {
4910
        $uid = (int)$uid;
4911
        if ($uid) {
4912
            $tableNames = $this->compileAdminTables();
4913
            foreach ($tableNames as $table) {
4914
                if ($table !== 'pages') {
4915
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
4916
                        ->getQueryBuilderForTable($table);
4917
4918
                    $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
4919
4920
                    $statement = $queryBuilder
4921
                        ->select('uid')
4922
                        ->from($table)
4923
                        ->where($queryBuilder->expr()->eq(
4924
                            'pid',
4925
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
4926
                        ))
4927
                        ->execute();
4928
4929
                    while ($row = $statement->fetch()) {
4930
                        $this->copyMovedRecordToNewLocation($table, $row['uid']);
4931
                        $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
4932
                        $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
4933
                    }
4934
                }
4935
            }
4936
            $this->copyMovedRecordToNewLocation('pages', $uid);
4937
            $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
4938
            $this->deleteRecord('pages', $uid, true, $forceHardDelete);
4939
        }
4940
    }
4941
4942
    /**
4943
     * Copies the move placeholder of a record to its new location (pid).
4944
     * This will create a "new" placeholder at the new location and
4945
     * a version for this new placeholder. The original move placeholder
4946
     * is then deleted because it is not needed anymore.
4947
     *
4948
     * This method is used to assure that moved records are not deleted
4949
     * when the origin page is deleted.
4950
     *
4951
     * @param string $table Record table
4952
     * @param int $uid Record uid
4953
     */
4954
    protected function copyMovedRecordToNewLocation($table, $uid)
4955
    {
4956
        if ($this->BE_USER->workspace > 0) {
4957
            $originalRecord = BackendUtility::getRecord($table, $uid);
4958
            $movePlaceholder = BackendUtility::getMovePlaceholder($table, $uid);
4959
            // Check whether target page to copied to is different to current page
4960
            // Cloning on the same page is superfluous and does not help at all
4961
            if (!empty($originalRecord) && !empty($movePlaceholder) && (int)$originalRecord['pid'] !== (int)$movePlaceholder['pid']) {
4962
                // If move placeholder exists, copy to new location
4963
                // This will create a New placeholder on the new location
4964
                // and a version for this new placeholder
4965
                $command = [
4966
                    $table => [
4967
                        $uid => [
4968
                            'copy' => '-' . $movePlaceholder['uid']
4969
                        ]
4970
                    ]
4971
                ];
4972
                /** @var DataHandler $dataHandler */
4973
                $dataHandler = GeneralUtility::makeInstance(__CLASS__);
4974
                $dataHandler->enableLogging = $this->enableLogging;
4975
                $dataHandler->neverHideAtCopy = true;
4976
                $dataHandler->start([], $command, $this->BE_USER);
4977
                $dataHandler->process_cmdmap();
4978
                unset($dataHandler);
4979
4980
                // Delete move placeholder
4981
                $this->deleteRecord($table, $movePlaceholder['uid'], true, true);
4982
            }
4983
        }
4984
    }
4985
4986
    /**
4987
     * Used to evaluate if a page can be deleted
4988
     *
4989
     * @param int $uid Page id
4990
     * @return int[]|string If array: List of page uids to traverse and delete (means OK), if string: error message.
4991
     */
4992
    public function canDeletePage($uid)
4993
    {
4994
        $uid = (int)$uid;
4995
        $isTranslatedPage = null;
4996
4997
        // If we may at all delete this page
4998
        // If this is a page translation, do the check against the perms_* of the default page
4999
        // Because it is currently only deleting the translation
5000
        $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
5001
        if ($defaultLanguagePageId !== $uid) {
5002
            if ($this->doesRecordExist('pages', (int)$defaultLanguagePageId, Permission::PAGE_DELETE)) {
5003
                $isTranslatedPage = true;
5004
            } else {
5005
                return 'Attempt to delete page without permissions';
5006
            }
5007
        } elseif (!$this->doesRecordExist('pages', $uid, Permission::PAGE_DELETE)) {
5008
            return 'Attempt to delete page without permissions';
5009
        }
5010
5011
        $pageIdsInBranch = $this->doesBranchExist('', $uid, Permission::PAGE_DELETE, true);
5012
5013
        if ($this->deleteTree) {
5014
            if ($pageIdsInBranch === -1) {
5015
                return 'Attempt to delete pages in branch without permissions';
5016
            }
5017
5018
            $pagesInBranch = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
5019
        } else {
5020
            if ($pageIdsInBranch === -1) {
5021
                return 'Attempt to delete page without permissions';
5022
            }
5023
            if ($pageIdsInBranch !== '') {
5024
                return 'Attempt to delete page which has subpages';
5025
            }
5026
5027
            $pagesInBranch = [$uid];
5028
        }
5029
5030
        if (!$this->checkForRecordsFromDisallowedTables($pagesInBranch)) {
5031
            return 'Attempt to delete records from disallowed tables';
5032
        }
5033
5034
        foreach ($pagesInBranch as $pageInBranch) {
5035
            if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, $isTranslatedPage ? false : true)) {
5036
                return 'Attempt to delete page which has prohibited localizations.';
5037
            }
5038
        }
5039
        return $pagesInBranch;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $pagesInBranch returns an array which contains values of type string which are incompatible with the documented value type integer.
Loading history...
5040
    }
5041
5042
    /**
5043
     * Returns TRUE if record CANNOT be deleted, otherwise FALSE. Used to check before the versioning API allows a record to be marked for deletion.
5044
     *
5045
     * @param string $table Record Table
5046
     * @param int $id Record UID
5047
     * @return string Returns a string IF there is an error (error string explaining). FALSE means record can be deleted
5048
     */
5049
    public function cannotDeleteRecord($table, $id)
5050
    {
5051
        if ($table === 'pages') {
5052
            $res = $this->canDeletePage($id);
5053
            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...
5054
        }
5055
        return $this->doesRecordExist($table, $id, Permission::PAGE_DELETE) ? 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...
5056
    }
5057
5058
    /**
5059
     * Determines whether a record can be undeleted.
5060
     *
5061
     * @param string $table Table name of the record
5062
     * @param int $uid uid of the record
5063
     * @return bool Whether the record can be undeleted
5064
     */
5065
    public function isRecordUndeletable($table, $uid)
5066
    {
5067
        $result = false;
5068
        $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5069
        if ($record['pid']) {
5070
            $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5071
            // The page containing the record is not deleted, thus the record can be undeleted:
5072
            if (!$page['deleted']) {
5073
                $result = true;
5074
            } else {
5075
                $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

5075
                $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...
5076
            }
5077
        } else {
5078
            // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5079
            $result = true;
5080
        }
5081
        return $result;
5082
    }
5083
5084
    /**
5085
     * Before a record is deleted, check if it has references such as inline type or MM references.
5086
     * If so, set these child records also to be deleted.
5087
     *
5088
     * @param string $table Record Table
5089
     * @param string $uid Record UID
5090
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5091
     * @see deleteRecord()
5092
     */
5093
    public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::deleteRecord_procFields" is not in camel caps format
Loading history...
5094
    {
5095
        $conf = $GLOBALS['TCA'][$table]['columns'];
5096
        $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

5096
        $row = BackendUtility::getRecord($table, /** @scrutinizer ignore-type */ $uid, '*', '', false);
Loading history...
5097
        if (empty($row)) {
5098
            return;
5099
        }
5100
        foreach ($row as $field => $value) {
5101
            $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5102
        }
5103
    }
5104
5105
    /**
5106
     * Process fields of a record to be deleted and search for special handling, like
5107
     * inline type, MM records, etc.
5108
     *
5109
     * @param string $table Record Table
5110
     * @param string $uid Record UID
5111
     * @param string $field Record field
5112
     * @param string $value Record field value
5113
     * @param array $conf TCA configuration of current field
5114
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5115
     * @see deleteRecord()
5116
     */
5117
    public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::deleteRecord_procBasedOnFieldType" is not in camel caps format
Loading history...
5118
    {
5119
        if ($conf['type'] === 'inline') {
5120
            $foreign_table = $conf['foreign_table'];
5121
            if ($foreign_table) {
5122
                $inlineType = $this->getInlineFieldType($conf);
5123
                if ($inlineType === 'list' || $inlineType === 'field') {
5124
                    /** @var RelationHandler $dbAnalysis */
5125
                    $dbAnalysis = $this->createRelationHandlerInstance();
5126
                    $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

5126
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
5127
                    $dbAnalysis->undeleteRecord = true;
5128
5129
                    $enableCascadingDelete = true;
5130
                    // non type save comparison is intended!
5131
                    if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5132
                        $enableCascadingDelete = false;
5133
                    }
5134
5135
                    // Walk through the items and remove them
5136
                    foreach ($dbAnalysis->itemArray as $v) {
5137
                        if (!$undeleteRecord) {
5138
                            if ($enableCascadingDelete) {
5139
                                $this->deleteAction($v['table'], $v['id']);
5140
                            }
5141
                        } else {
5142
                            $this->undeleteRecord($v['table'], $v['id']);
5143
                        }
5144
                    }
5145
                }
5146
            }
5147
        } elseif ($this->isReferenceField($conf)) {
5148
            $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5149
            $dbAnalysis = $this->createRelationHandlerInstance();
5150
            $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5151
            foreach ($dbAnalysis->itemArray as $v) {
5152
                $this->updateRefIndexStack[$table][$uid][] = [$v['table'], $v['id']];
5153
            }
5154
        }
5155
    }
5156
5157
    /**
5158
     * Find l10n-overlay records and perform the requested delete action for these records.
5159
     *
5160
     * @param string $table Record Table
5161
     * @param string $uid Record UID
5162
     */
5163
    public function deleteL10nOverlayRecords($table, $uid)
5164
    {
5165
        // Check whether table can be localized
5166
        if (!BackendUtility::isTableLocalizable($table)) {
5167
            return;
5168
        }
5169
5170
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5171
        $queryBuilder->getRestrictions()
5172
            ->removeAll()
5173
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5174
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
5175
5176
        $queryBuilder->select('*')
5177
            ->from($table)
5178
            ->where(
5179
                $queryBuilder->expr()->eq(
5180
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5181
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5182
                )
5183
            );
5184
5185
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
5186
            $queryBuilder->andWhere(
5187
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
5188
            );
5189
        }
5190
5191
        $result = $queryBuilder->execute();
5192
        while ($record = $result->fetch()) {
5193
            // Ignore workspace delete placeholders. Those records have been marked for
5194
            // deletion before - deleting them again in a workspace would revert that state.
5195
            if ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5196
                BackendUtility::workspaceOL($table, $record);
5197
                if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5198
                    continue;
5199
                }
5200
            }
5201
            $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5202
        }
5203
    }
5204
5205
    /*********************************************
5206
     *
5207
     * Cmd: Versioning
5208
     *
5209
     ********************************************/
5210
    /**
5211
     * Creates a new version of a record
5212
     * (Requires support in the table)
5213
     *
5214
     * @param string $table Table name
5215
     * @param int $id Record uid to versionize
5216
     * @param string $label Version label
5217
     * @param bool $delete If TRUE, the version is created to delete the record.
5218
     * @return int|null Returns the id of the new version (if any)
5219
     * @see copyRecord()
5220
     */
5221
    public function versionizeRecord($table, $id, $label, $delete = false)
5222
    {
5223
        $id = (int)$id;
5224
        // Stop any actions if the record is marked to be deleted:
5225
        // (this can occur if IRRE elements are versionized and child elements are removed)
5226
        if ($this->isElementToBeDeleted($table, $id)) {
5227
            return null;
5228
        }
5229
        if (!BackendUtility::isTableWorkspaceEnabled($table) || $id <= 0) {
5230
            $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, SystemLogErrorClassification::USER_ERROR);
5231
            return null;
5232
        }
5233
5234
        // Fetch record with permission check
5235
        $row = $this->recordInfoWithPermissionCheck($table, $id, Permission::PAGE_SHOW);
5236
5237
        // This checks if the record can be selected which is all that a copy action requires.
5238
        if ($row === false) {
5239
            $this->newlog(
5240
                'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "' . $table . ':' . $id . '"',
5241
                1
5242
            );
5243
            return null;
5244
        }
5245
5246
        // Record must be online record, otherwise we would create a version of a version
5247
        if ($row['t3ver_oid'] ?? 0 > 0) {
5248
            $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (record has an online ID)!', SystemLogErrorClassification::USER_ERROR);
5249
            return null;
5250
        }
5251
5252
        // Record must not be placeholder for moving.
5253
        if (VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
5254
            $this->newlog('Record cannot be versioned because it is a placeholder for a moving operation', SystemLogErrorClassification::USER_ERROR);
5255
            return null;
5256
        }
5257
5258
        if ($delete && $this->cannotDeleteRecord($table, $id)) {
5259
            $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), SystemLogErrorClassification::USER_ERROR);
5260
            return null;
5261
        }
5262
5263
        // Set up the values to override when making a raw-copy:
5264
        $overrideArray = [
5265
            't3ver_oid' => $id,
5266
            't3ver_wsid' => $this->BE_USER->workspace,
5267
            't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5268
            't3ver_count' => 0,
5269
            't3ver_stage' => 0,
5270
            't3ver_tstamp' => 0
5271
        ];
5272
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5273
            $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5274
        }
5275
        // Checking if the record already has a version in the current workspace of the backend user
5276
        $versionRecord = ['uid' => null];
5277
        if ($this->BE_USER->workspace !== 0) {
5278
            // Look for version already in workspace:
5279
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5280
        }
5281
        // Create new version of the record and return the new uid
5282
        if (empty($versionRecord['uid'])) {
5283
            // Create raw-copy and return result:
5284
            // The information of the label to be used for the workspace record
5285
            // as well as the information whether the record shall be removed
5286
            // must be forwarded (creating remove placeholders on a workspace are
5287
            // done by copying the record and override several fields).
5288
            $workspaceOptions = [
5289
                'delete' => $delete,
5290
                'label' => $label,
5291
            ];
5292
            return $this->copyRecord_raw($table, $id, -1, $overrideArray, $workspaceOptions);
5293
        }
5294
        // Reuse the existing record and return its uid
5295
        // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5296
        // did not make much sense since the information is available)
5297
        return $versionRecord['uid'];
5298
    }
5299
5300
    /**
5301
     * Swaps MM-relations for current/swap record, see version_swap()
5302
     *
5303
     * @param string $table Table for the two input records
5304
     * @param int $id Current record (about to go offline)
5305
     * @param int $swapWith Swap record (about to go online)
5306
     * @see version_swap()
5307
     */
5308
    public function version_remapMMForVersionSwap($table, $id, $swapWith)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::version_remapMMForVersionSwap" is not in camel caps format
Loading history...
5309
    {
5310
        // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5311
        $currentRec = BackendUtility::getRecord($table, $id);
5312
        $swapRec = BackendUtility::getRecord($table, $swapWith);
5313
        $this->version_remapMMForVersionSwap_reg = [];
5314
        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5315
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5316
            $conf = $fConf['config'];
5317
            if ($this->isReferenceField($conf)) {
5318
                $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5319
                $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5320
                if ($conf['MM']) {
5321
                    /** @var RelationHandler $dbAnalysis */
5322
                    $dbAnalysis = $this->createRelationHandlerInstance();
5323
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5324
                    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

5324
                    if (!empty($dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName))) {
Loading history...
5325
                        $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5326
                    }
5327
                    /** @var RelationHandler $dbAnalysis */
5328
                    $dbAnalysis = $this->createRelationHandlerInstance();
5329
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5330
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
5331
                        $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5332
                    }
5333
                }
5334
            } elseif ($conf['type'] === 'flex') {
5335
                // Current record
5336
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5337
                    $fConf,
5338
                    $table,
5339
                    $field,
5340
                    $currentRec
5341
                );
5342
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5343
                $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5344
                if (is_array($currentValueArray)) {
5345
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5346
                }
5347
                // Swap record
5348
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5349
                    $fConf,
5350
                    $table,
5351
                    $field,
5352
                    $swapRec
5353
                );
5354
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5355
                $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5356
                if (is_array($currentValueArray)) {
5357
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5358
                }
5359
            }
5360
        }
5361
        // Execute:
5362
        $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5363
    }
5364
5365
    /**
5366
     * Callback function for traversing the FlexForm structure in relation to ...
5367
     *
5368
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
5369
     * @param array $dsConf TCA field configuration (from Data Structure XML)
5370
     * @param string $dataValue The value of the flexForm field
5371
     * @param string $dataValue_ext1 Not used.
5372
     * @param string $dataValue_ext2 Not used.
5373
     * @param string $path Path in flexforms
5374
     * @see version_remapMMForVersionSwap()
5375
     * @see checkValue_flex_procInData_travDS()
5376
     */
5377
    public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::version_remapMMForVersionSwap_flexFormCallBack" is not in camel caps format
Loading history...
5378
    {
5379
        // Extract parameters:
5380
        [$table, $uid, $field] = $pParams;
5381
        if ($this->isReferenceField($dsConf)) {
5382
            $allowedTables = $dsConf['type'] === 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5383
            $prependName = $dsConf['type'] === 'group' ? $dsConf['prepend_tname'] : '';
5384
            if ($dsConf['MM']) {
5385
                /** @var RelationHandler $dbAnalysis */
5386
                $dbAnalysis = $this->createRelationHandlerInstance();
5387
                $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5388
                $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5389
            }
5390
        }
5391
    }
5392
5393
    /**
5394
     * Performing the remapping operations found necessary in version_remapMMForVersionSwap()
5395
     * 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.
5396
     *
5397
     * @param string $table Table for the two input records
5398
     * @param int $id Current record (about to go offline)
5399
     * @param int $swapWith Swap record (about to go online)
5400
     * @see version_remapMMForVersionSwap()
5401
     */
5402
    public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::version_remapMMForVersionSwap_execSwap" is not in camel caps format
Loading history...
5403
    {
5404
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5405
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5406
                $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5407
            }
5408
        }
5409
        if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5410
            foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5411
                $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5412
            }
5413
        }
5414
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5415
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5416
                $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5417
            }
5418
        }
5419
    }
5420
5421
    /*********************************************
5422
     *
5423
     * Cmd: Helper functions
5424
     *
5425
     ********************************************/
5426
5427
    /**
5428
     * Returns an instance of DataHandler for handling local datamaps/cmdmaps
5429
     *
5430
     * @return DataHandler
5431
     */
5432
    protected function getLocalTCE()
5433
    {
5434
        $copyTCE = GeneralUtility::makeInstance(DataHandler::class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
5435
        $copyTCE->copyTree = $this->copyTree;
5436
        $copyTCE->enableLogging = $this->enableLogging;
5437
        // Transformations should NOT be carried out during copy
5438
        $copyTCE->dontProcessTransformations = true;
5439
        // make sure the isImporting flag is transferred, so all hooks know if
5440
        // the current process is an import process
5441
        $copyTCE->isImporting = $this->isImporting;
5442
        $copyTCE->bypassAccessCheckForRecords = $this->bypassAccessCheckForRecords;
5443
        $copyTCE->bypassWorkspaceRestrictions = $this->bypassWorkspaceRestrictions;
5444
        return $copyTCE;
5445
    }
5446
5447
    /**
5448
     * Processes the fields with references as registered during the copy process. This includes all FlexForm fields which had references.
5449
     */
5450
    public function remapListedDBRecords()
5451
    {
5452
        if (!empty($this->registerDBList)) {
5453
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5454
            foreach ($this->registerDBList as $table => $records) {
5455
                foreach ($records as $uid => $fields) {
5456
                    $newData = [];
5457
                    $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5458
                    $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5459
                    foreach ($fields as $fieldName => $value) {
5460
                        $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5461
                        switch ($conf['type']) {
5462
                            case 'group':
5463
                            case 'select':
5464
                                $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5465
                                if (is_array($vArray)) {
5466
                                    $newData[$fieldName] = implode(',', $vArray);
5467
                                }
5468
                                break;
5469
                            case 'flex':
5470
                                if ($value === 'FlexForm_reference') {
5471
                                    // This will fetch the new row for the element
5472
                                    $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5473
                                    if (is_array($origRecordRow)) {
5474
                                        BackendUtility::workspaceOL($table, $origRecordRow);
5475
                                        // Get current data structure and value array:
5476
                                        $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5477
                                            ['config' => $conf],
5478
                                            $table,
5479
                                            $fieldName,
5480
                                            $origRecordRow
5481
                                        );
5482
                                        $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5483
                                        $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5484
                                        // Do recursive processing of the XML data:
5485
                                        $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5486
                                        // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5487
                                        if (is_array($currentValueArray['data'])) {
5488
                                            $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5489
                                        }
5490
                                    }
5491
                                }
5492
                                break;
5493
                            case 'inline':
5494
                                $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5495
                                break;
5496
                            default:
5497
                                $this->logger->debug('Field type should not appear here: ' . $conf['type']);
5498
                        }
5499
                    }
5500
                    // If any fields were changed, those fields are updated!
5501
                    if (!empty($newData)) {
5502
                        $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5503
                    }
5504
                }
5505
            }
5506
        }
5507
    }
5508
5509
    /**
5510
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
5511
     *
5512
     * @param array $pParams Set of parameters in numeric array: table, uid, field
5513
     * @param array $dsConf TCA config for field (from Data Structure of course)
5514
     * @param string $dataValue Field value (from FlexForm XML)
5515
     * @param string $dataValue_ext1 Not used
5516
     * @param string $dataValue_ext2 Not used
5517
     * @return array Array where the "value" key carries the value.
5518
     * @see checkValue_flex_procInData_travDS()
5519
     * @see remapListedDBRecords()
5520
     */
5521
    public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::remapListedDBRecords_flexFormCallBack" is not in camel caps format
Loading history...
5522
    {
5523
        // Extract parameters:
5524
        [$table, $uid, $field] = $pParams;
5525
        // If references are set for this field, set flag so they can be corrected later:
5526
        if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5527
            $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5528
            if (is_array($vArray)) {
5529
                $dataValue = implode(',', $vArray);
5530
            }
5531
        }
5532
        // Return
5533
        return ['value' => $dataValue];
5534
    }
5535
5536
    /**
5537
     * Performs remapping of old UID values to NEW uid values for a DB reference field.
5538
     *
5539
     * @param array $conf TCA field config
5540
     * @param string $value Field value
5541
     * @param int $MM_localUid UID of local record (for MM relations - might need to change if support for FlexForms should be done!)
5542
     * @param string $table Table name
5543
     * @return array|null Returns array of items ready to implode for field content.
5544
     * @see remapListedDBRecords()
5545
     */
5546
    public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::remapListedDBRecords_procDBRefs" is not in camel caps format
Loading history...
5547
    {
5548
        // Initialize variables
5549
        // Will be set TRUE if an upgrade should be done...
5550
        $set = false;
5551
        // Allowed tables for references.
5552
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5553
        // Table name to prepend the UID
5554
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5555
        // Which tables that should possibly not be remapped
5556
        $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5557
        // Convert value to list of references:
5558
        $dbAnalysis = $this->createRelationHandlerInstance();
5559
        $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && $conf['allowNonIdValues'];
5560
        $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5561
        // Traverse those references and map IDs:
5562
        foreach ($dbAnalysis->itemArray as $k => $v) {
5563
            $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5564
            if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5565
                $dbAnalysis->itemArray[$k]['id'] = $mapID;
5566
                $set = true;
5567
            }
5568
        }
5569
        if (!empty($conf['MM'])) {
5570
            // Purge invalid items (live/version)
5571
            $dbAnalysis->purgeItemArray();
5572
            if ($dbAnalysis->isPurged()) {
5573
                $set = true;
5574
            }
5575
5576
            // If record has been versioned/copied in this process, handle invalid relations of the live record
5577
            $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5578
            $originalId = 0;
5579
            if (!empty($this->copyMappingArray_merged[$table])) {
5580
                $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5581
            }
5582
            if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5583
                $liveRelations = $this->createRelationHandlerInstance();
5584
                $liveRelations->setWorkspaceId(0);
5585
                $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5586
                // Purge invalid relations in the live workspace ("0")
5587
                $liveRelations->purgeItemArray(0);
5588
                if ($liveRelations->isPurged()) {
5589
                    $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

5589
                    $liveRelations->writeMM($conf['MM'], $liveId, /** @scrutinizer ignore-type */ $prependName);
Loading history...
5590
                }
5591
            }
5592
        }
5593
        // If a change has been done, set the new value(s)
5594
        if ($set) {
5595
            if ($conf['MM']) {
5596
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5597
            } else {
5598
                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

5598
                return $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName);
Loading history...
5599
            }
5600
        }
5601
        return null;
5602
    }
5603
5604
    /**
5605
     * Performs remapping of old UID values to NEW uid values for an inline field.
5606
     *
5607
     * @param array $conf TCA field config
5608
     * @param string $value Field value
5609
     * @param int $uid The uid of the ORIGINAL record
5610
     * @param string $table Table name
5611
     */
5612
    public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::remapListedDBRecords_procInline" is not in camel caps format
Loading history...
5613
    {
5614
        $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5615
        if ($conf['foreign_table']) {
5616
            $inlineType = $this->getInlineFieldType($conf);
5617
            if ($inlineType === 'mm') {
5618
                $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5619
            } elseif ($inlineType !== false) {
5620
                /** @var RelationHandler $dbAnalysis */
5621
                $dbAnalysis = $this->createRelationHandlerInstance();
5622
                $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5623
5624
                // Keep original (live) item array and update values for specific versioned records
5625
                $originalItemArray = $dbAnalysis->itemArray;
5626
                foreach ($dbAnalysis->itemArray as &$item) {
5627
                    $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5628
                    if (!empty($versionedId)) {
5629
                        $item['id'] = $versionedId;
5630
                    }
5631
                }
5632
5633
                // Update child records if using pointer fields ('foreign_field'):
5634
                if ($inlineType === 'field') {
5635
                    $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
5636
                }
5637
                $thePidToUpdate = null;
5638
                // If the current field is set on a page record, update the pid of related child records:
5639
                if ($table === 'pages') {
5640
                    $thePidToUpdate = $theUidToUpdate;
5641
                } elseif (isset($this->registerDBPids[$table][$uid])) {
5642
                    $thePidToUpdate = $this->registerDBPids[$table][$uid];
5643
                    $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
5644
                }
5645
5646
                // Update child records if change to pid is required (only if the current record is not on a workspace):
5647
                if ($thePidToUpdate) {
5648
                    // Ensure that only the default language page is used as PID
5649
                    $thePidToUpdate = $this->getDefaultLanguagePageId($thePidToUpdate);
5650
                    // ensure, only live page ids are used as 'pid' values
5651
                    $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
5652
                    if ($liveId !== null) {
0 ignored issues
show
introduced by
The condition $liveId !== null is always true.
Loading history...
5653
                        $thePidToUpdate = $liveId;
5654
                    }
5655
                    $updateValues = ['pid' => $thePidToUpdate];
5656
                    foreach ($originalItemArray as $v) {
5657
                        if ($v['id'] && $v['table'] && BackendUtility::getLiveVersionIdOfRecord($v['table'], $v['id']) === null) {
5658
                            GeneralUtility::makeInstance(ConnectionPool::class)
5659
                                ->getConnectionForTable($v['table'])
5660
                                ->update($v['table'], $updateValues, ['uid' => (int)$v['id']]);
5661
                        }
5662
                    }
5663
                }
5664
            }
5665
        }
5666
    }
5667
5668
    /**
5669
     * Processes the $this->remapStack at the end of copying, inserting, etc. actions.
5670
     * The remapStack takes care about the correct mapping of new and old uids in case of relational data.
5671
     */
5672
    public function processRemapStack()
5673
    {
5674
        // Processes the remap stack:
5675
        if (is_array($this->remapStack)) {
0 ignored issues
show
introduced by
The condition is_array($this->remapStack) is always true.
Loading history...
5676
            $remapFlexForms = [];
5677
            $hookPayload = [];
5678
5679
            $newValue = null;
5680
            foreach ($this->remapStack as $remapAction) {
5681
                // If no position index for the arguments was set, skip this remap action:
5682
                if (!is_array($remapAction['pos'])) {
5683
                    continue;
5684
                }
5685
                // Load values from the argument array in remapAction:
5686
                $field = $remapAction['field'];
5687
                $id = $remapAction['args'][$remapAction['pos']['id']];
5688
                $rawId = $id;
5689
                $table = $remapAction['args'][$remapAction['pos']['table']];
5690
                $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
5691
                $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
5692
                $additionalData = $remapAction['additionalData'];
5693
                // The record is new and has one or more new ids (in case of versioning/workspaces):
5694
                if (strpos($id, 'NEW') !== false) {
5695
                    // Replace NEW...-ID with real uid:
5696
                    $id = $this->substNEWwithIDs[$id];
5697
                    // If the new parent record is on a non-live workspace or versionized, it has another new id:
5698
                    if (isset($this->autoVersionIdMap[$table][$id])) {
5699
                        $id = $this->autoVersionIdMap[$table][$id];
5700
                    }
5701
                    $remapAction['args'][$remapAction['pos']['id']] = $id;
5702
                }
5703
                // Replace relations to NEW...-IDs in field value (uids of child records):
5704
                if (is_array($valueArray)) {
5705
                    foreach ($valueArray as $key => $value) {
5706
                        if (strpos($value, 'NEW') !== false) {
5707
                            if (strpos($value, '_') === false) {
5708
                                $affectedTable = $tcaFieldConf['foreign_table'];
5709
                                $prependTable = false;
5710
                            } else {
5711
                                $parts = explode('_', $value);
5712
                                $value = array_pop($parts);
5713
                                $affectedTable = implode('_', $parts);
5714
                                $prependTable = true;
5715
                            }
5716
                            $value = $this->substNEWwithIDs[$value];
5717
                            // The record is new, but was also auto-versionized and has another new id:
5718
                            if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
5719
                                $value = $this->autoVersionIdMap[$affectedTable][$value];
5720
                            }
5721
                            if ($prependTable) {
5722
                                $value = $affectedTable . '_' . $value;
5723
                            }
5724
                            // Set a hint that this was a new child record:
5725
                            $this->newRelatedIDs[$affectedTable][] = $value;
5726
                            $valueArray[$key] = $value;
5727
                        }
5728
                    }
5729
                    $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
5730
                }
5731
                // Process the arguments with the defined function:
5732
                if (!empty($remapAction['func'])) {
5733
                    $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
5734
                }
5735
                // If array is returned, check for maxitems condition, if string is returned this was already done:
5736
                if (is_array($newValue)) {
5737
                    $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
5738
                    // The reference casting is only required if
5739
                    // checkValue_group_select_processDBdata() returns an array
5740
                    $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
5741
                }
5742
                // Update in database (list of children (csv) or number of relations (foreign_field)):
5743
                if (!empty($field)) {
5744
                    $fieldArray = [$field => $newValue];
5745
                    if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
5746
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
5747
                    }
5748
                    $this->updateDB($table, $id, $fieldArray);
5749
                } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
5750
                    // Collect data to update FlexForms
5751
                    $flexFormId = $additionalData['flexFormId'];
5752
                    $flexFormPath = $additionalData['flexFormPath'];
5753
5754
                    if (!isset($remapFlexForms[$flexFormId])) {
5755
                        $remapFlexForms[$flexFormId] = [];
5756
                    }
5757
5758
                    $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
5759
                }
5760
5761
                // Collect elements that shall trigger processDatamap_afterDatabaseOperations
5762
                if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
5763
                    $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
5764
                    if (!isset($hookPayload[$table][$rawId])) {
5765
                        $hookPayload[$table][$rawId] = [
5766
                            'status' => $hookArgs['status'],
5767
                            'fieldArray' => $hookArgs['fieldArray'],
5768
                            'hookObjects' => $hookArgs['hookObjectsArr'],
5769
                        ];
5770
                    }
5771
                    $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
5772
                }
5773
            }
5774
5775
            if ($remapFlexForms) {
5776
                foreach ($remapFlexForms as $flexFormId => $modifications) {
5777
                    $this->updateFlexFormData($flexFormId, $modifications);
5778
                }
5779
            }
5780
5781
            foreach ($hookPayload as $tableName => $rawIdPayload) {
5782
                foreach ($rawIdPayload as $rawId => $payload) {
5783
                    foreach ($payload['hookObjects'] as $hookObject) {
5784
                        if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
5785
                            continue;
5786
                        }
5787
                        $hookObject->processDatamap_afterDatabaseOperations(
5788
                            $payload['status'],
5789
                            $tableName,
5790
                            $rawId,
5791
                            $payload['fieldArray'],
5792
                            $this
5793
                        );
5794
                    }
5795
                }
5796
            }
5797
        }
5798
        // Processes the remap stack actions:
5799
        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...
5800
            foreach ($this->remapStackActions as $action) {
5801
                if (isset($action['callback'], $action['arguments'])) {
5802
                    call_user_func_array($action['callback'], $action['arguments']);
5803
                }
5804
            }
5805
        }
5806
        // Processes the reference index updates of the remap stack:
5807
        foreach ($this->remapStackRefIndex as $table => $idArray) {
5808
            foreach ($idArray as $id) {
5809
                $this->updateRefIndex($table, $id);
5810
                unset($this->remapStackRefIndex[$table][$id]);
5811
            }
5812
        }
5813
        // Reset:
5814
        $this->remapStack = [];
5815
        $this->remapStackRecords = [];
5816
        $this->remapStackActions = [];
5817
        $this->remapStackRefIndex = [];
5818
    }
5819
5820
    /**
5821
     * Updates FlexForm data.
5822
     *
5823
     * @param string $flexFormId e.g. <table>:<uid>:<field>
5824
     * @param array $modifications Modifications with paths and values (e.g. 'sDEF/lDEV/field/vDEF' => 'TYPO3')
5825
     */
5826
    protected function updateFlexFormData($flexFormId, array $modifications)
5827
    {
5828
        [$table, $uid, $field] = explode(':', $flexFormId, 3);
5829
5830
        if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
5831
            $uid = $this->substNEWwithIDs[$uid];
5832
        }
5833
5834
        $record = $this->recordInfo($table, $uid, '*');
5835
5836
        if (!$table || !$uid || !$field || !is_array($record)) {
5837
            return;
5838
        }
5839
5840
        BackendUtility::workspaceOL($table, $record);
5841
5842
        // Get current data structure and value array:
5843
        $valueStructure = GeneralUtility::xml2array($record[$field]);
5844
5845
        // Do recursive processing of the XML data:
5846
        foreach ($modifications as $path => $value) {
5847
            $valueStructure['data'] = ArrayUtility::setValueByPath(
5848
                $valueStructure['data'],
5849
                $path,
5850
                $value
5851
            );
5852
        }
5853
5854
        if (is_array($valueStructure['data'])) {
5855
            // The return value should be compiled back into XML
5856
            $values = [
5857
                $field => $this->checkValue_flexArray2Xml($valueStructure, true),
5858
            ];
5859
5860
            $this->updateDB($table, $uid, $values);
5861
        }
5862
    }
5863
5864
    /**
5865
     * Triggers a remap action for a specific record.
5866
     *
5867
     * Some records are post-processed by the processRemapStack() method (e.g. IRRE children).
5868
     * This method determines whether an action/modification is executed directly to a record
5869
     * or is postponed to happen after remapping data.
5870
     *
5871
     * @param string $table Name of the table
5872
     * @param string $id Id of the record (can also be a "NEW..." string)
5873
     * @param array $callback The method to be called
5874
     * @param array $arguments The arguments to be submitted to the callback method
5875
     * @param bool $forceRemapStackActions Whether to force to use the stack
5876
     * @see processRemapStack
5877
     */
5878
    protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
5879
    {
5880
        // Check whether the affected record is marked to be remapped:
5881
        if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
5882
            call_user_func_array($callback, $arguments);
5883
        } else {
5884
            $this->addRemapAction($table, $id, $callback, $arguments);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Core\DataHandl...ndler::addRemapAction(). ( Ignorable by Annotation )

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

5884
            $this->addRemapAction($table, /** @scrutinizer ignore-type */ $id, $callback, $arguments);
Loading history...
5885
        }
5886
    }
5887
5888
    /**
5889
     * Adds an instruction to the remap action stack (used with IRRE).
5890
     *
5891
     * @param string $table The affected table
5892
     * @param int $id The affected ID
5893
     * @param array $callback The callback information (object and method)
5894
     * @param array $arguments The arguments to be used with the callback
5895
     */
5896
    public function addRemapAction($table, $id, array $callback, array $arguments)
5897
    {
5898
        $this->remapStackActions[] = [
5899
            'affects' => [
5900
                'table' => $table,
5901
                'id' => $id
5902
            ],
5903
            'callback' => $callback,
5904
            'arguments' => $arguments
5905
        ];
5906
    }
5907
5908
    /**
5909
     * Adds a table-id-pair to the reference index remapping stack.
5910
     *
5911
     * @param string $table
5912
     * @param int $id
5913
     */
5914
    public function addRemapStackRefIndex($table, $id)
5915
    {
5916
        $this->remapStackRefIndex[$table][$id] = $id;
5917
    }
5918
5919
    /**
5920
     * If a parent record was versionized on a workspace in $this->process_datamap,
5921
     * it might be possible, that child records (e.g. on using IRRE) were affected.
5922
     * This function finds these relations and updates their uids in the $incomingFieldArray.
5923
     * The $incomingFieldArray is updated by reference!
5924
     *
5925
     * @param string $table Table name of the parent record
5926
     * @param int $id Uid of the parent record
5927
     * @param array $incomingFieldArray Reference to the incomingFieldArray of process_datamap
5928
     * @param array $registerDBList Reference to the $registerDBList array that was created/updated by versionizing calls to DataHandler in process_datamap.
5929
     */
5930
    public function getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
5931
    {
5932
        if (is_array($registerDBList[$table][$id])) {
5933
            foreach ($incomingFieldArray as $field => $value) {
5934
                $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
5935
                if ($registerDBList[$table][$id][$field] && ($foreignTable = $fieldConf['foreign_table'])) {
5936
                    $newValueArray = [];
5937
                    $origValueArray = is_array($value) ? $value : explode(',', $value);
5938
                    // Update the uids of the copied records, but also take care about new records:
5939
                    foreach ($origValueArray as $childId) {
5940
                        $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ?: $childId;
5941
                    }
5942
                    // Set the changed value to the $incomingFieldArray
5943
                    $incomingFieldArray[$field] = implode(',', $newValueArray);
5944
                }
5945
            }
5946
            // Clean up the $registerDBList array:
5947
            unset($registerDBList[$table][$id]);
5948
            if (empty($registerDBList[$table])) {
5949
                unset($registerDBList[$table]);
5950
            }
5951
        }
5952
    }
5953
5954
    /*****************************
5955
     *
5956
     * Access control / Checking functions
5957
     *
5958
     *****************************/
5959
    /**
5960
     * Checking group modify_table access list
5961
     *
5962
     * @param string $table Table name
5963
     * @return bool Returns TRUE if the user has general access to modify the $table
5964
     */
5965
    public function checkModifyAccessList($table)
5966
    {
5967
        $res = $this->admin || (!$this->tableAdminOnly($table) && isset($this->BE_USER->groupData['tables_modify']) && GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table));
5968
        // Hook 'checkModifyAccessList': Post-processing of the state of access
5969
        foreach ($this->getCheckModifyAccessListHookObjects() as $hookObject) {
5970
            /** @var DataHandlerCheckModifyAccessListHookInterface $hookObject */
5971
            $hookObject->checkModifyAccessList($res, $table, $this);
5972
        }
5973
        return $res;
5974
    }
5975
5976
    /**
5977
     * Checking if a record with uid $id from $table is in the BE_USERS webmounts which is required for editing etc.
5978
     *
5979
     * @param string $table Table name
5980
     * @param int $id UID of record
5981
     * @return bool Returns TRUE if OK. Cached results.
5982
     */
5983
    public function isRecordInWebMount($table, $id)
5984
    {
5985
        if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
5986
            $recP = $this->getRecordProperties($table, $id);
5987
            $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
5988
        }
5989
        return $this->isRecordInWebMount_Cache[$table . ':' . $id];
5990
    }
5991
5992
    /**
5993
     * Checks if the input page ID is in the BE_USER webmounts
5994
     *
5995
     * @param int $pid Page ID to check
5996
     * @return bool TRUE if OK. Cached results.
5997
     */
5998
    public function isInWebMount($pid)
5999
    {
6000
        if (!isset($this->isInWebMount_Cache[$pid])) {
6001
            $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
6002
        }
6003
        return $this->isInWebMount_Cache[$pid];
6004
    }
6005
6006
    /**
6007
     * Checks if user may update a record with uid=$id from $table
6008
     *
6009
     * @param string $table Record table
6010
     * @param int $id Record UID
6011
     * @param array|bool $data Record data
6012
     * @param array $hookObjectsArr Hook objects
6013
     * @return bool Returns TRUE if the user may update the record given by $table and $id
6014
     */
6015
    public function checkRecordUpdateAccess($table, $id, $data = false, $hookObjectsArr = null)
6016
    {
6017
        $res = null;
6018
        if (is_array($hookObjectsArr)) {
6019
            foreach ($hookObjectsArr as $hookObj) {
6020
                if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
6021
                    $res = $hookObj->checkRecordUpdateAccess($table, $id, $data, $res, $this);
6022
                }
6023
            }
6024
            if (isset($res)) {
6025
                return (bool)$res;
6026
            }
6027
        }
6028
        $res = false;
6029
6030
        if ($GLOBALS['TCA'][$table] && (int)$id > 0) {
6031
            $cacheId = 'checkRecordUpdateAccess_' . $table . '_' . $id;
6032
6033
            // If information is cached, return it
6034
            $cachedValue = $this->runtimeCache->get($cacheId);
6035
            if (!empty($cachedValue)) {
6036
                return $cachedValue;
6037
            }
6038
6039
            if ($table === 'pages' || ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap))) {
6040
                // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6041
                $perms = Permission::PAGE_EDIT;
6042
            } else {
6043
                $perms = Permission::CONTENT_EDIT;
6044
            }
6045
            if ($this->doesRecordExist($table, $id, $perms)) {
6046
                $res = 1;
6047
            }
6048
            // Cache the result
6049
            $this->runtimeCache->set($cacheId, $res);
6050
        }
6051
        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...
6052
    }
6053
6054
    /**
6055
     * Checks if user may insert a record from $insertTable on $pid
6056
     * Does not check for workspace, use BE_USER->workspaceAllowLiveRecordsInPID for this in addition to this function call.
6057
     *
6058
     * @param string $insertTable Tablename to check
6059
     * @param int $pid Integer PID
6060
     * @param int $action For logging: Action number.
6061
     * @return bool Returns TRUE if the user may insert a record from table $insertTable on page $pid
6062
     */
6063
    public function checkRecordInsertAccess($insertTable, $pid, $action = SystemLogDatabaseAction::INSERT)
6064
    {
6065
        $pid = (int)$pid;
6066
        if ($pid < 0) {
6067
            return false;
6068
        }
6069
        // If information is cached, return it
6070
        if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
6071
            return $this->recInsertAccessCache[$insertTable][$pid];
6072
        }
6073
6074
        $res = false;
6075
        if ($insertTable === 'pages') {
6076
            $perms = Permission::PAGE_NEW;
6077
        } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
6078
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6079
            $perms = Permission::PAGE_EDIT;
6080
        } else {
6081
            $perms = Permission::CONTENT_EDIT;
6082
        }
6083
        $pageExists = (bool)$this->doesRecordExist('pages', $pid, $perms);
6084
        // 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
6085
        if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
6086
            // Check permissions
6087
            if ($this->isTableAllowedForThisPage($pid, $insertTable)) {
6088
                $res = true;
6089
                // Cache the result
6090
                $this->recInsertAccessCache[$insertTable][$pid] = $res;
6091
            } elseif ($this->enableLogging) {
6092
                $propArr = $this->getRecordProperties('pages', $pid);
6093
                $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']);
6094
            }
6095
        } elseif ($this->enableLogging) {
6096
            $propArr = $this->getRecordProperties('pages', $pid);
6097
            $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']);
6098
        }
6099
        return $res;
6100
    }
6101
6102
    /**
6103
     * 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.
6104
     *
6105
     * @param int $page_uid Page id for which to check, including 0 (zero) if checking for page tree root.
6106
     * @param string $checkTable Table name to check
6107
     * @return bool TRUE if OK
6108
     */
6109
    public function isTableAllowedForThisPage($page_uid, $checkTable)
6110
    {
6111
        $page_uid = (int)$page_uid;
6112
        $rootLevelSetting = (int)$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'];
6113
        // 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.
6114
        if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$page_uid)) {
6115
            return false;
6116
        }
6117
        $allowed = false;
6118
        // Check root-level
6119
        if (!$page_uid) {
6120
            if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
6121
                $allowed = true;
6122
            }
6123
        } else {
6124
            // Check non-root-level
6125
            $doktype = $this->pageInfo($page_uid, 'doktype');
6126
            $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6127
            $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6128
            // If all tables or the table is listed as an allowed type, return TRUE
6129
            if (strpos($allowedTableList, '*') !== false || in_array($checkTable, $allowedArray, true)) {
6130
                $allowed = true;
6131
            }
6132
        }
6133
        return $allowed;
6134
    }
6135
6136
    /**
6137
     * Checks if record can be selected based on given permission criteria
6138
     *
6139
     * @param string $table Record table name
6140
     * @param int $id Record UID
6141
     * @param int|string $perms Permission restrictions to observe: Either an integer that will be bitwise AND'ed or a string, which points to a key in the ->pMap array. Only integers are supported starting with TYPO3 v11.
6142
     * @return bool Returns TRUE if the record given by $table, $id and $perms can be selected
6143
     *
6144
     * @throws \RuntimeException
6145
     */
6146
    public function doesRecordExist($table, $id, $perms)
6147
    {
6148
        if (!MathUtility::canBeInterpretedAsInteger($perms)) {
6149
            trigger_error('Support for handing in permissions as string into "doesRecordExist" will be not be supported with TYPO3 v11 anymore. Use the Permission BitSet class instead.', E_USER_DEPRECATED);
6150
        }
6151
        return $this->recordInfoWithPermissionCheck($table, $id, $perms, 'uid, pid') !== false;
6152
    }
6153
6154
    /**
6155
     * Looks up a page based on permissions.
6156
     *
6157
     * @param int $id Page id
6158
     * @param int $perms Permission integer
6159
     * @param array $columns Columns to select
6160
     * @return bool|array
6161
     * @internal
6162
     * @see doesRecordExist()
6163
     */
6164
    protected function doesRecordExist_pageLookUp($id, $perms, $columns = ['uid'])
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::doesRecordExist_pageLookUp" is not in camel caps format
Loading history...
6165
    {
6166
        $cacheId = md5('doesRecordExist_pageLookUp_' . $id . '_' . $perms . '_' . implode(
6167
            '_',
6168
            $columns
6169
        ) . '_' . (string)$this->admin);
6170
6171
        // If result is cached, return it
6172
        $cachedResult = $this->runtimeCache->get($cacheId);
6173
        if (!empty($cachedResult)) {
6174
            return $cachedResult;
6175
        }
6176
6177
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6178
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6179
        $queryBuilder
6180
            ->select(...$columns)
6181
            ->from('pages')
6182
            ->where($queryBuilder->expr()->eq(
6183
                'uid',
6184
                $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
6185
            ));
6186
        if ($perms && !$this->admin) {
6187
            $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($perms));
6188
        }
6189
        if (!$this->admin && $GLOBALS['TCA']['pages']['ctrl']['editlock'] &&
6190
            $perms & Permission::PAGE_EDIT + Permission::PAGE_DELETE + Permission::CONTENT_EDIT
6191
        ) {
6192
            $queryBuilder->andWhere($queryBuilder->expr()->eq(
6193
                $GLOBALS['TCA']['pages']['ctrl']['editlock'],
6194
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
6195
            ));
6196
        }
6197
6198
        $row = $queryBuilder->execute()->fetch();
6199
        $this->runtimeCache->set($cacheId, $row);
6200
6201
        return $row;
6202
    }
6203
6204
    /**
6205
     * Checks if a whole branch of pages exists
6206
     *
6207
     * Tests the branch under $pid like doesRecordExist(), but it doesn't test the page with $pid as uid - use doesRecordExist() for this purpose.
6208
     * 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
6209
     *
6210
     * @param string $inList List of page uids, this is added to and returned in the end
6211
     * @param int $pid Page ID to select subpages from.
6212
     * @param int $perms Perms integer to check each page record for.
6213
     * @param bool $recurse Recursion flag: If set, it will go out through the branch.
6214
     * @return string|int List of page IDs in branch, if there are subpages, empty string if there are none or -1 if no permission
6215
     */
6216
    public function doesBranchExist($inList, $pid, $perms, $recurse)
6217
    {
6218
        $pid = (int)$pid;
6219
        $perms = (int)$perms;
6220
        if ($pid >= 0) {
6221
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6222
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6223
            $result = $queryBuilder
6224
                ->select('uid', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody')
6225
                ->from('pages')
6226
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
6227
                ->orderBy('sorting')
6228
                ->execute();
6229
            while ($row = $result->fetch()) {
6230
                // IF admin, then it's OK
6231
                if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $perms)) {
6232
                    $inList .= $row['uid'] . ',';
6233
                    if ($recurse) {
6234
                        // Follow the subpages recursively...
6235
                        $inList = $this->doesBranchExist($inList, $row['uid'], $perms, $recurse);
6236
                        if ($inList === -1) {
6237
                            return -1;
6238
                        }
6239
                    }
6240
                } else {
6241
                    // No permissions
6242
                    return -1;
6243
                }
6244
            }
6245
        }
6246
        return $inList;
6247
    }
6248
6249
    /**
6250
     * Checks if the $table is readOnly
6251
     *
6252
     * @param string $table Table name
6253
     * @return bool TRUE, if readonly
6254
     */
6255
    public function tableReadOnly($table)
6256
    {
6257
        // Returns TRUE if table is readonly
6258
        return (bool)$GLOBALS['TCA'][$table]['ctrl']['readOnly'];
6259
    }
6260
6261
    /**
6262
     * Checks if the $table is only editable by admin-users
6263
     *
6264
     * @param string $table Table name
6265
     * @return bool TRUE, if readonly
6266
     */
6267
    public function tableAdminOnly($table)
6268
    {
6269
        // Returns TRUE if table is admin-only
6270
        return !empty($GLOBALS['TCA'][$table]['ctrl']['adminOnly']);
6271
    }
6272
6273
    /**
6274
     * Checks if page $id is a uid in the rootline of page id $destinationId
6275
     * Used when moving a page
6276
     *
6277
     * @param int $destinationId Destination Page ID to test
6278
     * @param int $id Page ID to test for presence inside Destination
6279
     * @return bool Returns FALSE if ID is inside destination (including equal to)
6280
     */
6281
    public function destNotInsideSelf($destinationId, $id)
6282
    {
6283
        $loopCheck = 100;
6284
        $destinationId = (int)$destinationId;
6285
        $id = (int)$id;
6286
        if ($destinationId === $id) {
6287
            return false;
6288
        }
6289
        while ($destinationId !== 0 && $loopCheck > 0) {
6290
            $loopCheck--;
6291
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6292
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6293
            $result = $queryBuilder
6294
                ->select('pid', 'uid', 't3ver_oid', 't3ver_wsid')
6295
                ->from('pages')
6296
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($destinationId, \PDO::PARAM_INT)))
6297
                ->execute();
6298
            if ($row = $result->fetch()) {
6299
                BackendUtility::fixVersioningPid('pages', $row);
6300
                if ($row['pid'] == $id) {
6301
                    return false;
6302
                }
6303
                $destinationId = (int)$row['pid'];
6304
            } else {
6305
                return false;
6306
            }
6307
        }
6308
        return true;
6309
    }
6310
6311
    /**
6312
     * 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
6313
     * Will also generate this list for admin-users so they must be check for before calling the function
6314
     *
6315
     * @return array Array of [table]-[field] pairs to exclude from editing.
6316
     */
6317
    public function getExcludeListArray()
6318
    {
6319
        $list = [];
6320
        if (isset($this->BE_USER->groupData['non_exclude_fields'])) {
6321
            $nonExcludeFieldsArray = array_flip(GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
6322
            foreach ($GLOBALS['TCA'] as $table => $tableConfiguration) {
6323
                if (isset($tableConfiguration['columns'])) {
6324
                    foreach ($tableConfiguration['columns'] as $field => $config) {
6325
                        if ($config['exclude'] && !isset($nonExcludeFieldsArray[$table . ':' . $field])) {
6326
                            $list[] = $table . '-' . $field;
6327
                        }
6328
                    }
6329
                }
6330
            }
6331
        }
6332
6333
        return $list;
6334
    }
6335
6336
    /**
6337
     * Checks if there are records on a page from tables that are not allowed
6338
     *
6339
     * @param int $page_uid Page ID
6340
     * @param int $doktype Page doktype
6341
     * @return bool|array Returns a list of the tables that are 'present' on the page but not allowed with the page_uid/doktype
6342
     */
6343
    public function doesPageHaveUnallowedTables($page_uid, $doktype)
6344
    {
6345
        $page_uid = (int)$page_uid;
6346
        if (!$page_uid) {
6347
            // Not a number. Probably a new page
6348
            return false;
6349
        }
6350
        $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6351
        // If all tables are allowed, return early
6352
        if (strpos($allowedTableList, '*') !== false) {
6353
            return false;
6354
        }
6355
        $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6356
        $tableList = [];
6357
        $allTableNames = $this->compileAdminTables();
6358
        foreach ($allTableNames as $table) {
6359
            // If the table is not in the allowed list, check if there are records...
6360
            if (in_array($table, $allowedArray, true)) {
6361
                continue;
6362
            }
6363
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6364
            $queryBuilder->getRestrictions()->removeAll();
6365
            $count = $queryBuilder
6366
                ->count('uid')
6367
                ->from($table)
6368
                ->where($queryBuilder->expr()->eq(
6369
                    'pid',
6370
                    $queryBuilder->createNamedParameter($page_uid, \PDO::PARAM_INT)
6371
                ))
6372
                ->execute()
6373
                ->fetchColumn(0);
6374
            if ($count) {
6375
                $tableList[] = $table;
6376
            }
6377
        }
6378
        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...
6379
    }
6380
6381
    /*****************************
6382
     *
6383
     * Information lookup
6384
     *
6385
     *****************************/
6386
    /**
6387
     * Returns the value of the $field from page $id
6388
     * 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!
6389
     *
6390
     * @param int $id Page uid
6391
     * @param string $field Field name for which to return value
6392
     * @return string Value of the field. Result is cached in $this->pageCache[$id][$field] and returned from there next time!
6393
     */
6394
    public function pageInfo($id, $field)
6395
    {
6396
        if (!isset($this->pageCache[$id])) {
6397
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6398
            $queryBuilder->getRestrictions()->removeAll();
6399
            $row = $queryBuilder
6400
                ->select('*')
6401
                ->from('pages')
6402
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6403
                ->execute()
6404
                ->fetch();
6405
            if ($row) {
6406
                $this->pageCache[$id] = $row;
6407
            }
6408
        }
6409
        return $this->pageCache[$id][$field];
6410
    }
6411
6412
    /**
6413
     * Returns the row of a record given by $table and $id and $fieldList (list of fields, may be '*')
6414
     * NOTICE: No check for deleted or access!
6415
     *
6416
     * @param string $table Table name
6417
     * @param int $id UID of the record from $table
6418
     * @param string $fieldList Field list for the SELECT query, eg. "*" or "uid,pid,...
6419
     * @return array|null Returns the selected record on success, otherwise NULL.
6420
     */
6421
    public function recordInfo($table, $id, $fieldList)
6422
    {
6423
        // Skip, if searching for NEW records or there's no TCA table definition
6424
        if ((int)$id === 0 || !isset($GLOBALS['TCA'][$table])) {
6425
            return null;
6426
        }
6427
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6428
        $queryBuilder->getRestrictions()->removeAll();
6429
        $result = $queryBuilder
6430
            ->select(...GeneralUtility::trimExplode(',', $fieldList))
6431
            ->from($table)
6432
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6433
            ->execute()
6434
            ->fetch();
6435
        return $result ?: null;
6436
    }
6437
6438
    /**
6439
     * Checks if record exists with and without permission check and returns that row
6440
     *
6441
     * @param string $table Record table name
6442
     * @param int $id Record UID
6443
     * @param int|string $perms Permission restrictions to observe: Either an integer that will be bitwise AND'ed or a string, which points to a key in the ->pMap array. With TYPO3 v11, only integers are allowed
6444
     * @param string $fieldList - fields - default is '*'
6445
     * @throws \RuntimeException
6446
     * @return array|bool Row if exists and accessible, false otherwise
6447
     */
6448
    protected function recordInfoWithPermissionCheck(string $table, int $id, $perms, string $fieldList = '*')
6449
    {
6450
        if ($this->bypassAccessCheckForRecords) {
6451
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6452
6453
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6454
            $queryBuilder->getRestrictions()->removeAll();
6455
6456
            $record = $queryBuilder->select(...$columns)
6457
                ->from($table)
6458
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6459
                ->execute()
6460
                ->fetch();
6461
6462
            return $record ?: false;
6463
        }
6464
        // Processing the incoming $perms (from possible string to integer that can be AND'ed)
6465
        if (!MathUtility::canBeInterpretedAsInteger($perms)) {
6466
            trigger_error('Support for handing in permissions as string into "recordInfoWithPermissionCheck" will be not be supported with TYPO3 v11 anymore. Use the Permission BitSet class instead.', E_USER_DEPRECATED);
6467
            if ($table !== 'pages') {
6468
                switch ($perms) {
6469
                    case 'edit':
6470
                    case 'delete':
6471
                    case 'new':
6472
                        // This holds it all in case the record is not page!!
6473
                        if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
6474
                            $perms = 'edit';
6475
                        } else {
6476
                            $perms = 'editcontent';
6477
                        }
6478
                        break;
6479
                }
6480
            }
6481
            $perms = (int)(Permission::getMap()[$perms] ?? 0);
6482
        } else {
6483
            $perms = (int)$perms;
6484
        }
6485
        if (!$perms) {
6486
            throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
6487
        }
6488
        // For all tables: Check if record exists:
6489
        $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
6490
        if (is_array($GLOBALS['TCA'][$table]) && $id > 0 && ($this->admin || $isWebMountRestrictionIgnored || $this->isRecordInWebMount($table, $id))) {
6491
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6492
            if ($table !== 'pages') {
6493
                // Find record without checking page
6494
                // @todo: This should probably check for editlock
6495
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6496
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6497
                $output = $queryBuilder
6498
                    ->select(...$columns)
6499
                    ->from($table)
6500
                    ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6501
                    ->execute()
6502
                    ->fetch();
6503
                BackendUtility::fixVersioningPid($table, $output, true);
6504
                // If record found, check page as well:
6505
                if (is_array($output)) {
6506
                    // Looking up the page for record:
6507
                    $pageRec = $this->doesRecordExist_pageLookUp($output['pid'], $perms);
6508
                    // 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):
6509
                    $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
6510
                    if (is_array($pageRec) || !$output['pid'] && ($this->admin || $isRootLevelRestrictionIgnored)) {
6511
                        return $output;
6512
                    }
6513
                }
6514
                return false;
6515
            }
6516
            return $this->doesRecordExist_pageLookUp($id, $perms, $columns);
6517
        }
6518
        return false;
6519
    }
6520
6521
    /**
6522
     * Returns an array with record properties, like header and pid
6523
     * No check for deleted or access is done!
6524
     * For versionized records, pid is resolved to its live versions pid.
6525
     * Used for logging
6526
     *
6527
     * @param string $table Table name
6528
     * @param int $id Uid of record
6529
     * @param bool $noWSOL If set, no workspace overlay is performed
6530
     * @return array Properties of record
6531
     */
6532
    public function getRecordProperties($table, $id, $noWSOL = false)
6533
    {
6534
        $row = $table === 'pages' && !$id ? ['title' => '[root-level]', 'uid' => 0, 'pid' => 0] : $this->recordInfo($table, $id, '*');
6535
        if (!$noWSOL) {
6536
            BackendUtility::workspaceOL($table, $row);
6537
        }
6538
        return $this->getRecordPropertiesFromRow($table, $row);
6539
    }
6540
6541
    /**
6542
     * Returns an array with record properties, like header and pid, based on the row
6543
     *
6544
     * @param string $table Table name
6545
     * @param array $row Input row
6546
     * @return array|null Output array
6547
     */
6548
    public function getRecordPropertiesFromRow($table, $row)
6549
    {
6550
        if ($GLOBALS['TCA'][$table]) {
6551
            BackendUtility::fixVersioningPid($table, $row);
6552
            $liveUid = ($row['t3ver_oid'] ?? null) ? $row['t3ver_oid'] : $row['uid'];
6553
            return [
6554
                'header' => BackendUtility::getRecordTitle($table, $row),
6555
                'pid' => $row['pid'],
6556
                'event_pid' => $this->eventPid($table, (int)$liveUid, $row['pid']),
6557
                't3ver_state' => BackendUtility::isTableWorkspaceEnabled($table) ? $row['t3ver_state'] : '',
6558
                '_ORIG_pid' => $row['_ORIG_pid']
6559
            ];
6560
        }
6561
        return null;
6562
    }
6563
6564
    /**
6565
     * @param string $table
6566
     * @param int $uid
6567
     * @param int $pid
6568
     * @return int
6569
     */
6570
    public function eventPid($table, $uid, $pid)
6571
    {
6572
        return $table === 'pages' ? $uid : $pid;
6573
    }
6574
6575
    /*********************************************
6576
     *
6577
     * Storing data to Database Layer
6578
     *
6579
     ********************************************/
6580
    /**
6581
     * Update database record
6582
     * Does not check permissions but expects them to be verified on beforehand
6583
     *
6584
     * @param string $table Record table name
6585
     * @param int $id Record uid
6586
     * @param array $fieldArray Array of field=>value pairs to insert. FIELDS MUST MATCH the database FIELDS. No check is done.
6587
     */
6588
    public function updateDB($table, $id, $fieldArray)
6589
    {
6590
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && (int)$id) {
6591
            // Do NOT update the UID field, ever!
6592
            unset($fieldArray['uid']);
6593
            if (!empty($fieldArray)) {
6594
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6595
6596
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6597
6598
                $types = [];
6599
                $platform = $connection->getDatabasePlatform();
6600
                if ($platform instanceof SQLServerPlatform) {
6601
                    // mssql needs to set proper PARAM_LOB and others to update fields
6602
                    $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
6603
                    foreach ($fieldArray as $columnName => $columnValue) {
6604
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
6605
                    }
6606
                }
6607
6608
                // Execute the UPDATE query:
6609
                $updateErrorMessage = '';
6610
                try {
6611
                    $connection->update($table, $fieldArray, ['uid' => (int)$id], $types);
6612
                } catch (DBALException $e) {
6613
                    $updateErrorMessage = $e->getPrevious()->getMessage();
6614
                }
6615
                // If succeeds, do...:
6616
                if ($updateErrorMessage === '') {
6617
                    // Update reference index:
6618
                    $this->updateRefIndex($table, $id);
6619
                    // Set History data
6620
                    $historyEntryId = 0;
6621
                    if (isset($this->historyRecords[$table . ':' . $id])) {
6622
                        $historyEntryId = $this->getRecordHistoryStore()->modifyRecord($table, $id, $this->historyRecords[$table . ':' . $id], $this->correlationId);
6623
                    }
6624
                    if ($this->enableLogging) {
6625
                        if ($this->checkStoredRecords) {
6626
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, SystemLogDatabaseAction::UPDATE);
6627
                        } else {
6628
                            $newRow = $fieldArray;
6629
                            $newRow['uid'] = $id;
6630
                        }
6631
                        // Set log entry:
6632
                        $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6633
                        $isOfflineVersion = (bool)($newRow['t3ver_oid'] ?? 0);
6634
                        $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']);
6635
                    }
6636
                    // Clear cache for relevant pages:
6637
                    $this->registerRecordIdForPageCacheClearing($table, $id);
6638
                    // Unset the pageCache for the id if table was page.
6639
                    if ($table === 'pages') {
6640
                        unset($this->pageCache[$id]);
6641
                    }
6642
                } else {
6643
                    $this->log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$updateErrorMessage, $table . ':' . $id]);
6644
                }
6645
            }
6646
        }
6647
    }
6648
6649
    /**
6650
     * Insert into database
6651
     * Does not check permissions but expects them to be verified on beforehand
6652
     *
6653
     * @param string $table Record table name
6654
     * @param string $id "NEW...." uid string
6655
     * @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!
6656
     * @param bool $newVersion Set to TRUE if new version is created.
6657
     * @param int $suggestedUid Suggested UID value for the inserted record. See the array $this->suggestedInsertUids; Admin-only feature
6658
     * @param bool $dontSetNewIdIndex If TRUE, the ->substNEWwithIDs array is not updated. Only useful in very rare circumstances!
6659
     * @return int|null Returns ID on success.
6660
     */
6661
    public function insertDB($table, $id, $fieldArray, $newVersion = false, $suggestedUid = 0, $dontSetNewIdIndex = false)
6662
    {
6663
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && isset($fieldArray['pid'])) {
6664
            // Do NOT insert the UID field, ever!
6665
            unset($fieldArray['uid']);
6666
            if (!empty($fieldArray)) {
6667
                // Check for "suggestedUid".
6668
                // This feature is used by the import functionality to force a new record to have a certain UID value.
6669
                // This is only recommended for use when the destination server is a passive mirror of another server.
6670
                // As a security measure this feature is available only for Admin Users (for now)
6671
                $suggestedUid = (int)$suggestedUid;
6672
                if ($this->BE_USER->isAdmin() && $suggestedUid && $this->suggestedInsertUids[$table . ':' . $suggestedUid]) {
6673
                    // When the value of ->suggestedInsertUids[...] is "DELETE" it will try to remove the previous record
6674
                    if ($this->suggestedInsertUids[$table . ':' . $suggestedUid] === 'DELETE') {
6675
                        // DELETE:
6676
                        GeneralUtility::makeInstance(ConnectionPool::class)
6677
                            ->getConnectionForTable($table)
6678
                            ->delete($table, ['uid' => (int)$suggestedUid]);
6679
                    }
6680
                    $fieldArray['uid'] = $suggestedUid;
6681
                }
6682
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6683
                $typeArray = [];
6684
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])
6685
                    && array_key_exists($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
6686
                ) {
6687
                    $typeArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = Connection::PARAM_LOB;
6688
                }
6689
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6690
                $insertErrorMessage = '';
6691
                try {
6692
                    // Execute the INSERT query:
6693
                    $connection->insert(
6694
                        $table,
6695
                        $fieldArray,
6696
                        $typeArray
6697
                    );
6698
                } catch (DBALException $e) {
6699
                    $insertErrorMessage = $e->getPrevious()->getMessage();
6700
                }
6701
                // If succees, do...:
6702
                if ($insertErrorMessage === '') {
6703
                    // Set mapping for NEW... -> real uid:
6704
                    // the NEW_id now holds the 'NEW....' -id
6705
                    $NEW_id = $id;
6706
                    $id = $this->postProcessDatabaseInsert($connection, $table, $suggestedUid);
6707
6708
                    if (!$dontSetNewIdIndex) {
6709
                        $this->substNEWwithIDs[$NEW_id] = $id;
6710
                        $this->substNEWwithIDs_table[$NEW_id] = $table;
6711
                    }
6712
                    $newRow = [];
6713
                    if ($this->enableLogging) {
6714
                        // Checking the record is properly saved if configured
6715
                        if ($this->checkStoredRecords) {
6716
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, SystemLogDatabaseAction::INSERT);
6717
                        } else {
6718
                            $newRow = $fieldArray;
6719
                            $newRow['uid'] = $id;
6720
                        }
6721
                    }
6722
                    // Update reference index:
6723
                    $this->updateRefIndex($table, $id);
6724
6725
                    // Store in history
6726
                    $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

6726
                    $this->getRecordHistoryStore()->addRecord($table, $id, /** @scrutinizer ignore-type */ $newRow, $this->correlationId);
Loading history...
6727
6728
                    if ($newVersion) {
6729
                        if ($this->enableLogging) {
6730
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6731
                            $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);
6732
                        }
6733
                    } else {
6734
                        if ($this->enableLogging) {
6735
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6736
                            $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
6737
                            $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);
6738
                        }
6739
                        // Clear cache for relevant pages:
6740
                        $this->registerRecordIdForPageCacheClearing($table, $id);
6741
                    }
6742
                    return $id;
6743
                }
6744
                if ($this->enableLogging) {
6745
                    $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

6745
                    $this->log($table, /** @scrutinizer ignore-type */ $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
Loading history...
6746
                }
6747
            }
6748
        }
6749
        return null;
6750
    }
6751
6752
    /**
6753
     * Checking stored record to see if the written values are properly updated.
6754
     *
6755
     * @param string $table Record table name
6756
     * @param int $id Record uid
6757
     * @param array $fieldArray Array of field=>value pairs to insert/update
6758
     * @param string $action Action, for logging only.
6759
     * @return array|null Selected row
6760
     * @see insertDB()
6761
     * @see updateDB()
6762
     */
6763
    public function checkStoredRecord($table, $id, $fieldArray, $action)
6764
    {
6765
        $id = (int)$id;
6766
        if (is_array($GLOBALS['TCA'][$table]) && $id) {
6767
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6768
            $queryBuilder->getRestrictions()->removeAll();
6769
6770
            $row = $queryBuilder
6771
                ->select('*')
6772
                ->from($table)
6773
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6774
                ->execute()
6775
                ->fetch();
6776
6777
            if (!empty($row)) {
6778
                // Traverse array of values that was inserted into the database and compare with the actually stored value:
6779
                $errors = [];
6780
                foreach ($fieldArray as $key => $value) {
6781
                    if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
6782
                        if (is_float($row[$key])) {
6783
                            // if the database returns the value as double, compare it as double
6784
                            if ((double)$value !== (double)$row[$key]) {
6785
                                $errors[] = $key;
6786
                            }
6787
                        } else {
6788
                            $dbType = $GLOBALS['TCA'][$table]['columns'][$key]['config']['dbType'] ?? false;
6789
                            if ($dbType === 'datetime' || $dbType === 'time') {
6790
                                $row[$key] = $this->normalizeTimeFormat($table, $row[$key], $dbType);
6791
                            }
6792
                            if ((string)$value !== (string)$row[$key]) {
6793
                                // The is_numeric check catches cases where we want to store a float/double value
6794
                                // and database returns the field as a string with the least required amount of
6795
                                // significant digits, i.e. "0.00" being saved and "0" being read back.
6796
                                if (is_numeric($value) && is_numeric($row[$key])) {
6797
                                    if ((double)$value === (double)$row[$key]) {
6798
                                        continue;
6799
                                    }
6800
                                }
6801
                                $errors[] = $key;
6802
                            }
6803
                        }
6804
                    }
6805
                }
6806
                // Set log message if there were fields with unmatching values:
6807
                if (!empty($errors)) {
6808
                    $message = sprintf(
6809
                        '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.',
6810
                        $id,
6811
                        $table,
6812
                        implode(', ', $errors)
6813
                    );
6814
                    $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

6814
                    $this->log($table, $id, /** @scrutinizer ignore-type */ $action, 0, SystemLogErrorClassification::USER_ERROR, $message);
Loading history...
6815
                }
6816
                // Return selected rows:
6817
                return $row;
6818
            }
6819
        }
6820
        return null;
6821
    }
6822
6823
    /**
6824
     * Setting sys_history record, based on content previously set in $this->historyRecords[$table . ':' . $id] (by compareFieldArrayWithCurrentAndUnset())
6825
     *
6826
     * This functionality is now moved into the RecordHistoryStore and can be used instead.
6827
     *
6828
     * @param string $table Table name
6829
     * @param int $id Record ID
6830
     * @param int $logId Log entry ID, important for linking between log and history views
6831
     */
6832
    public function setHistory($table, $id, $logId)
6833
    {
6834
        if (isset($this->historyRecords[$table . ':' . $id])) {
6835
            $this->getRecordHistoryStore()->modifyRecord(
6836
                $table,
6837
                $id,
6838
                $this->historyRecords[$table . ':' . $id],
6839
                $this->correlationId
6840
            );
6841
        }
6842
    }
6843
6844
    /**
6845
     * @return RecordHistoryStore
6846
     */
6847
    protected function getRecordHistoryStore(): RecordHistoryStore
6848
    {
6849
        return GeneralUtility::makeInstance(
6850
            RecordHistoryStore::class,
6851
            RecordHistoryStore::USER_BACKEND,
6852
            $this->BE_USER->user['uid'],
6853
            $this->BE_USER->user['ses_backuserid'] ?? null,
6854
            $GLOBALS['EXEC_TIME'],
6855
            $this->BE_USER->workspace
6856
        );
6857
    }
6858
6859
    /**
6860
     * Update Reference Index (sys_refindex) for a record
6861
     * Should be called any almost any update to a record which could affect references inside the record.
6862
     *
6863
     * @param string $table Table name
6864
     * @param int $id Record UID
6865
     */
6866
    public function updateRefIndex($table, $id)
6867
    {
6868
        /** @var ReferenceIndex $refIndexObj */
6869
        $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
6870
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
6871
            $refIndexObj->setWorkspaceId($this->BE_USER->workspace);
6872
        }
6873
        $refIndexObj->enableRuntimeCache();
6874
        $refIndexObj->updateRefIndexTable($table, $id);
6875
    }
6876
6877
    /*********************************************
6878
     *
6879
     * Misc functions
6880
     *
6881
     ********************************************/
6882
    /**
6883
     * Returning sorting number for tables with a "sortby" column
6884
     * Using when new records are created and existing records are moved around.
6885
     *
6886
     * The strategy is:
6887
     *  - if no record exists: set interval as sorting number
6888
     *  - if inserted before an element: put in the middle of the existing elements
6889
     *  - if inserted behind the last element: add interval to last sorting number
6890
     *  - if collision: move all subsequent records by 2 * interval, insert new record with collision + interval
6891
     *
6892
     * How to calculate the maximum possible inserts for the worst case of adding all records to the top,
6893
     * such that the sorting number stays within INT_MAX
6894
     *
6895
     * i = interval (currently 256)
6896
     * c = number of inserts until collision
6897
     * s = max sorting number to reach (INT_MAX - 32bit)
6898
     * n = number of records (~83 million)
6899
     *
6900
     * c = 2 * g
6901
     * g = log2(i) / 2 + 1
6902
     * n = g * s / i - g + 1
6903
     *
6904
     * The algorithm can be tuned by adjusting the interval value.
6905
     * Higher value means less collisions, but also less inserts are possible to stay within INT_MAX.
6906
     *
6907
     * @param string $table Table name
6908
     * @param int $uid Uid of record to find sorting number for. May be zero in case of new.
6909
     * @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)
6910
     * @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.
6911
     */
6912
    public function getSortNumber($table, $uid, $pid)
6913
    {
6914
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
6915
        if (!$sortColumn) {
6916
            return null;
6917
        }
6918
6919
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
6920
        $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
6921
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6922
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->BE_USER->workspace));
6923
6924
        $queryBuilder
6925
            ->select($sortColumn, 'pid', 'uid')
6926
            ->from($table);
6927
6928
        // find and return the sorting value for the first record on that pid
6929
        if ($pid >= 0) {
6930
            // Fetches the first record (lowest sorting) under this pid
6931
            $row = $queryBuilder
6932
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
6933
                ->orderBy($sortColumn, 'ASC')
6934
                ->addOrderBy('uid', 'ASC')
6935
                ->setMaxResults(1)
6936
                ->execute()
6937
                ->fetch();
6938
6939
            if (!empty($row)) {
6940
                // The top record was the record itself, so we return its current sorting value
6941
                if ($row['uid'] == $uid) {
6942
                    return $row[$sortColumn];
6943
                }
6944
                // If the record sorting value < 1 we must resort all the records under this pid
6945
                if ($row[$sortColumn] < 1) {
6946
                    $this->increaseSortingOfFollowingRecords($table, (int)$pid);
6947
                    // Lowest sorting value after full resorting is $sortIntervals
6948
                    return $this->sortIntervals;
6949
                }
6950
                // Sorting number between current top element and zero
6951
                return floor($row[$sortColumn] / 2);
6952
            }
6953
            // No records, so we choose the default value as sorting-number
6954
            return $this->sortIntervals;
6955
        }
6956
6957
        // Find and return first possible sorting value AFTER record with given uid ($pid)
6958
        // Fetches the record which is supposed to be the prev record
6959
        $row = $queryBuilder
6960
                ->where($queryBuilder->expr()->eq(
6961
                    'uid',
6962
                    $queryBuilder->createNamedParameter(abs($pid), \PDO::PARAM_INT)
6963
                ))
6964
                ->execute()
6965
                ->fetch();
6966
6967
        // There is a previous record
6968
        if (!empty($row)) {
6969
            // Look, if the record UID happens to be an offline record. If so, find its live version.
6970
            // Offline uids will be used when a page is versionized as "branch" so this is when we must correct
6971
            // - otherwise a pid of "-1" and a wrong sort-row number is returned which we don't want.
6972
            if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortColumn . ',pid,uid')) {
6973
                $row = $lookForLiveVersion;
6974
            }
6975
            // Fetch move placeholder, since it might point to a new page in the current workspace
6976
            if ($movePlaceholder = BackendUtility::getMovePlaceholder($table, $row['uid'], 'uid,pid,' . $sortColumn)) {
6977
                $row = $movePlaceholder;
6978
            }
6979
            // If the record should be inserted after itself, keep the current sorting information:
6980
            if ((int)$row['uid'] === (int)$uid) {
6981
                $sortNumber = $row[$sortColumn];
6982
            } else {
6983
                $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
6984
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6985
                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->BE_USER->workspace));
6986
6987
                $subResults = $queryBuilder
6988
                        ->select($sortColumn, 'pid', 'uid')
6989
                        ->from($table)
6990
                        ->where(
6991
                            $queryBuilder->expr()->eq(
6992
                                'pid',
6993
                                $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT)
6994
                            ),
6995
                            $queryBuilder->expr()->gte(
6996
                                $sortColumn,
6997
                                $queryBuilder->createNamedParameter($row[$sortColumn], \PDO::PARAM_INT)
6998
                            )
6999
                        )
7000
                        ->orderBy($sortColumn, 'ASC')
7001
                        ->addOrderBy('uid', 'DESC')
7002
                        ->setMaxResults(2)
7003
                        ->execute()
7004
                        ->fetchAll();
7005
                // Fetches the next record in order to calculate the in-between sortNumber
7006
                // There was a record afterwards
7007
                if (count($subResults) === 2) {
7008
                    // There was a record afterwards, fetch that
7009
                    $subrow = array_pop($subResults);
7010
                    // The sortNumber is found in between these values
7011
                    $sortNumber = $row[$sortColumn] + floor(($subrow[$sortColumn] - $row[$sortColumn]) / 2);
7012
                    // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
7013
                    if ($sortNumber <= $row[$sortColumn] || $sortNumber >= $subrow[$sortColumn]) {
7014
                        $this->increaseSortingOfFollowingRecords($table, (int)$row['pid'], (int)$row[$sortColumn]);
7015
                        $sortNumber = $row[$sortColumn] + $this->sortIntervals;
7016
                    }
7017
                } else {
7018
                    // If after the last record in the list, we just add the sortInterval to the last sortvalue
7019
                    $sortNumber = $row[$sortColumn] + $this->sortIntervals;
7020
                }
7021
            }
7022
            return ['pid' => $row['pid'], 'sortNumber' => $sortNumber];
7023
        }
7024
        if ($this->enableLogging) {
7025
            $propArr = $this->getRecordProperties($table, $uid);
7026
            // OK, don't insert $propArr['event_pid'] here...
7027
            $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']);
7028
        }
7029
        // There MUST be a previous record or else this cannot work
7030
        return false;
7031
    }
7032
7033
    /**
7034
     * Increases sorting field value of all records with sorting higher than $sortingValue
7035
     *
7036
     * Used internally by getSortNumber() to "make space" in sorting values when inserting new record
7037
     *
7038
     * @param string $table Table name
7039
     * @param int $pid Page Uid in which to resort records
7040
     * @param int $sortingValue All sorting numbers larger than this number will be shifted
7041
     * @see getSortNumber()
7042
     */
7043
    protected function increaseSortingOfFollowingRecords(string $table, int $pid, int $sortingValue = null): void
7044
    {
7045
        $sortBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7046
        if ($sortBy) {
7047
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7048
7049
            $queryBuilder
7050
                ->update($table)
7051
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7052
                ->set($sortBy, $queryBuilder->quoteIdentifier($sortBy) . ' + ' . $this->sortIntervals . ' + ' . $this->sortIntervals, false);
7053
            if ($sortingValue !== null) {
7054
                $queryBuilder->andWhere($queryBuilder->expr()->gt($sortBy, $sortingValue));
7055
            }
7056
7057
            $deleteColumn = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? '';
7058
            if ($deleteColumn) {
7059
                $queryBuilder->andWhere($queryBuilder->expr()->eq($deleteColumn, 0));
7060
            }
7061
7062
            $queryBuilder->execute();
7063
        }
7064
    }
7065
7066
    /**
7067
     * Returning uid of previous localized record, if any, for tables with a "sortby" column
7068
     * Used when new localized records are created so that localized records are sorted in the same order as the default language records
7069
     *
7070
     * For a given record (A) uid (record we're translating) it finds first default language record (from the same colpos)
7071
     * with sorting smaller than given record (B).
7072
     * Then it fetches a translated version of record B and returns it's uid.
7073
     *
7074
     * If there is no record B, or it has no translation in given language, the record A uid is returned.
7075
     * The localized record will be placed the after record which uid is returned.
7076
     *
7077
     * @param string $table Table name
7078
     * @param int $uid Uid of default language record
7079
     * @param int $pid Pid of default language record
7080
     * @param int $language Language of localization
7081
     * @return int uid of record after which the localized record should be inserted
7082
     */
7083
    protected function getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
7084
    {
7085
        $previousLocalizedRecordUid = $uid;
7086
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7087
        if ($sortColumn) {
7088
            $select = [$sortColumn, 'pid', 'uid'];
7089
            // For content elements, we also need the colPos
7090
            if ($table === 'tt_content') {
7091
                $select[] = 'colPos';
7092
            }
7093
            // Get the sort value of the default language record
7094
            $row = BackendUtility::getRecord($table, $uid, implode(',', $select));
7095
            if (is_array($row)) {
7096
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7097
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7098
7099
                $queryBuilder
7100
                    ->select(...$select)
7101
                    ->from($table)
7102
                    ->where(
7103
                        $queryBuilder->expr()->eq(
7104
                            'pid',
7105
                            $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
7106
                        ),
7107
                        $queryBuilder->expr()->eq(
7108
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
7109
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
7110
                        ),
7111
                        $queryBuilder->expr()->lt(
7112
                            $sortColumn,
7113
                            $queryBuilder->createNamedParameter($row[$sortColumn], \PDO::PARAM_INT)
7114
                        )
7115
                    )
7116
                    ->orderBy($sortColumn, 'DESC')
7117
                    ->addOrderBy('uid', 'DESC')
7118
                    ->setMaxResults(1);
7119
                if ($table === 'tt_content') {
7120
                    $queryBuilder
7121
                        ->andWhere(
7122
                            $queryBuilder->expr()->eq(
7123
                                'colPos',
7124
                                $queryBuilder->createNamedParameter($row['colPos'], \PDO::PARAM_INT)
7125
                            )
7126
                        );
7127
                }
7128
                // If there is an element, find its localized record in specified localization language
7129
                if ($previousRow = $queryBuilder->execute()->fetch()) {
7130
                    $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $language);
7131
                    if (is_array($previousLocalizedRecord[0])) {
7132
                        $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
7133
                    }
7134
                }
7135
            }
7136
        }
7137
        return $previousLocalizedRecordUid;
7138
    }
7139
7140
    /**
7141
     * Setting up perms_* fields in $fieldArray based on TSconfig input
7142
     * Used for new pages
7143
     *
7144
     * @param array $fieldArray Field Array, returned with modifications
7145
     * @param array $TSConfig_p TSconfig properties
7146
     * @return array Modified Field Array
7147
     * @deprecated will be removed in TYPO3 v11.0 - Use PagePermissionAssembler instead.
7148
     */
7149
    public function setTSconfigPermissions($fieldArray, $TSConfig_p)
7150
    {
7151
        trigger_error('DataHandler->setTSconfigPermissions will be removed in TYPO3 v11.0. Use the PagePermissionAssembler API instead.', E_USER_DEPRECATED);
7152
        if ((string)$TSConfig_p['userid'] !== '') {
7153
            $fieldArray['perms_userid'] = (int)$TSConfig_p['userid'];
7154
        }
7155
        if ((string)$TSConfig_p['groupid'] !== '') {
7156
            $fieldArray['perms_groupid'] = (int)$TSConfig_p['groupid'];
7157
        }
7158
        if ((string)$TSConfig_p['user'] !== '') {
7159
            $fieldArray['perms_user'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['user']) ? $TSConfig_p['user'] : $this->assemblePermissions($TSConfig_p['user']);
7160
        }
7161
        if ((string)$TSConfig_p['group'] !== '') {
7162
            $fieldArray['perms_group'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['group']) ? $TSConfig_p['group'] : $this->assemblePermissions($TSConfig_p['group']);
7163
        }
7164
        if ((string)$TSConfig_p['everybody'] !== '') {
7165
            $fieldArray['perms_everybody'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['everybody']) ? $TSConfig_p['everybody'] : $this->assemblePermissions($TSConfig_p['everybody']);
7166
        }
7167
        return $fieldArray;
7168
    }
7169
7170
    /**
7171
     * 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.
7172
     * Used for new records and during copy operations for defaults
7173
     *
7174
     * @param string $table Table name for which to set default values.
7175
     * @return array Array with default values.
7176
     */
7177
    public function newFieldArray($table)
7178
    {
7179
        $fieldArray = [];
7180
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
7181
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $content) {
7182
                if (isset($this->defaultValues[$table][$field])) {
7183
                    $fieldArray[$field] = $this->defaultValues[$table][$field];
7184
                } elseif (isset($content['config']['default'])) {
7185
                    $fieldArray[$field] = $content['config']['default'];
7186
                }
7187
            }
7188
        }
7189
        return $fieldArray;
7190
    }
7191
7192
    /**
7193
     * 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.
7194
     *
7195
     * @param string $table Table name
7196
     * @param array $incomingFieldArray Incoming array (passed by reference)
7197
     */
7198
    public function addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
7199
    {
7200
        // Checking languages:
7201
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
7202
            if (!isset($incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
7203
                // Language field must be found in input row - otherwise it does not make sense.
7204
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
7205
                    ->getQueryBuilderForTable('sys_language');
7206
                $queryBuilder->getRestrictions()
7207
                    ->removeAll()
7208
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7209
                $queryBuilder
7210
                    ->select('uid')
7211
                    ->from('sys_language')
7212
                    ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
7213
                $rows = array_merge([['uid' => 0]], $queryBuilder->execute()->fetchAll(), [['uid' => -1]]);
7214
                foreach ($rows as $r) {
7215
                    if ($this->BE_USER->checkLanguageAccess($r['uid'])) {
7216
                        $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $r['uid'];
7217
                        break;
7218
                    }
7219
                }
7220
            }
7221
        }
7222
    }
7223
7224
    /**
7225
     * Returns the $data array from $table overridden in the fields defined in ->overrideValues.
7226
     *
7227
     * @param string $table Table name
7228
     * @param array $data Data array with fields from table. These will be overlaid with values in $this->overrideValues[$table]
7229
     * @return array Data array, processed.
7230
     */
7231
    public function overrideFieldArray($table, $data)
7232
    {
7233
        if (is_array($this->overrideValues[$table])) {
7234
            $data = array_merge($data, $this->overrideValues[$table]);
7235
        }
7236
        return $data;
7237
    }
7238
7239
    /**
7240
     * Compares the incoming field array with the current record and unsets all fields which are the same.
7241
     * Used for existing records being updated
7242
     *
7243
     * @param string $table Record table name
7244
     * @param int $id Record uid
7245
     * @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!
7246
     * @return array Returns $fieldArray. If the returned array is empty, then the record should not be updated!
7247
     */
7248
    public function compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
7249
    {
7250
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7251
        $queryBuilder = $connection->createQueryBuilder();
7252
        $queryBuilder->getRestrictions()->removeAll();
7253
        $currentRecord = $queryBuilder->select('*')
7254
            ->from($table)
7255
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7256
            ->execute()
7257
            ->fetch();
7258
        // If the current record exists (which it should...), begin comparison:
7259
        if (is_array($currentRecord)) {
7260
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
7261
            $columnRecordTypes = [];
7262
            foreach ($currentRecord as $columnName => $_) {
7263
                $columnRecordTypes[$columnName] = '';
7264
                $type = $tableDetails->getColumn($columnName)->getType();
7265
                if ($type instanceof IntegerType) {
7266
                    $columnRecordTypes[$columnName] = 'int';
7267
                }
7268
            }
7269
            // Unset the fields which are similar:
7270
            foreach ($fieldArray as $col => $val) {
7271
                $fieldConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
7272
                $isNullField = (!empty($fieldConfiguration['eval']) && GeneralUtility::inList($fieldConfiguration['eval'], 'null'));
7273
7274
                // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
7275
                // In general this avoids to store superfluous data which also will be visualized in the editing history.
7276
                if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $columnRecordTypes[$col], $isNullField)) {
7277
                    unset($fieldArray[$col]);
7278
                } else {
7279
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
7280
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
7281
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
7282
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
7283
                    }
7284
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
7285
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
7286
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
7287
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
7288
                    }
7289
                }
7290
            }
7291
        } else {
7292
            // If the current record does not exist this is an error anyways and we just return an empty array here.
7293
            $fieldArray = [];
7294
        }
7295
        return $fieldArray;
7296
    }
7297
7298
    /**
7299
     * Determines whether submitted values and stored values are equal.
7300
     * This prevents from adding superfluous field changes which would be shown in the record history as well.
7301
     * For NULL fields (see accordant TCA definition 'eval' = 'null'), a special handling is required since
7302
     * (!strcmp(NULL, '')) would be a false-positive.
7303
     *
7304
     * @param mixed $submittedValue Value that has submitted (e.g. from a backend form)
7305
     * @param mixed $storedValue Value that is currently stored in the database
7306
     * @param string $storedType SQL type of the stored value column (see mysql_field_type(), e.g 'int', 'string',  ...)
7307
     * @param bool $allowNull Whether NULL values are allowed by accordant TCA definition ('eval' = 'null')
7308
     * @return bool Whether both values are considered to be equal
7309
     */
7310
    protected function isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
7311
    {
7312
        // No NULL values are allowed, this is the regular behaviour.
7313
        // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
7314
        if (!$allowNull) {
7315
            $result = (string)$submittedValue === (string)$storedValue || $storedType === 'int' && (int)$storedValue === (int)$submittedValue;
7316
        // Null values are allowed, but currently there's a real (not NULL) value.
7317
        // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
7318
        } elseif ($storedValue !== null) {
7319
            $result = (
7320
                $submittedValue !== null
7321
                && $this->isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
7322
            );
7323
        // Null values are allowed, and currently there's a NULL value.
7324
        // Thus, check whether a NULL value was submitted.
7325
        } else {
7326
            $result = ($submittedValue === null);
7327
        }
7328
7329
        return $result;
7330
    }
7331
7332
    /**
7333
     * Calculates the bitvalue of the permissions given in a string, comma-separated
7334
     *
7335
     * @param string $string List of pMap strings
7336
     * @return int Integer mask
7337
     * @see setTSconfigPermissions()
7338
     * @see newFieldArray()
7339
     * @deprecated will be removed in TYPO3 v11.0 - Use PagePermissionAssembler instead.
7340
     */
7341
    public function assemblePermissions($string)
7342
    {
7343
        trigger_error('DataHandler->assemblePermissions will be removed in TYPO3 v11.0. Use the PagePermissionAssembler API instead.', E_USER_DEPRECATED);
7344
        $keyArr = GeneralUtility::trimExplode(',', $string, true);
7345
        $value = 0;
7346
        $permissionMap = Permission::getMap();
7347
        foreach ($keyArr as $key) {
7348
            if ($key && isset($permissionMap[$key])) {
7349
                $value |= $permissionMap[$key];
7350
            }
7351
        }
7352
        return $value;
7353
    }
7354
7355
    /**
7356
     * Converts a HTML entity (like &#123;) to the character '123'
7357
     *
7358
     * @param string $input Input string
7359
     * @return string Output string
7360
     */
7361
    public function convNumEntityToByteValue($input)
7362
    {
7363
        $token = md5(microtime());
7364
        $parts = explode($token, preg_replace('/(&#([0-9]+);)/', $token . '\\2' . $token, $input));
7365
        foreach ($parts as $k => $v) {
7366
            if ($k % 2) {
7367
                $v = (int)$v;
7368
                // Just to make sure that control bytes are not converted.
7369
                if ($v > 32) {
7370
                    $parts[$k] = chr($v);
7371
                }
7372
            }
7373
        }
7374
        return implode('', $parts);
7375
    }
7376
7377
    /**
7378
     * Disables the delete clause for fetching records.
7379
     * In general only undeleted records will be used. If the delete
7380
     * clause is disabled, also deleted records are taken into account.
7381
     */
7382
    public function disableDeleteClause()
7383
    {
7384
        $this->disableDeleteClause = true;
7385
    }
7386
7387
    /**
7388
     * Returns delete-clause for the $table
7389
     *
7390
     * @param string $table Table name
7391
     * @return string Delete clause
7392
     */
7393
    public function deleteClause($table)
7394
    {
7395
        // Returns the proper delete-clause if any for a table from TCA
7396
        if (!$this->disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
7397
            return ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
7398
        }
7399
        return '';
7400
    }
7401
7402
    /**
7403
     * Add delete restriction if not disabled
7404
     *
7405
     * @param QueryRestrictionContainerInterface $restrictions
7406
     */
7407
    protected function addDeleteRestriction(QueryRestrictionContainerInterface $restrictions)
7408
    {
7409
        if (!$this->disableDeleteClause) {
7410
            $restrictions->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7411
        }
7412
    }
7413
7414
    /**
7415
     * Gets UID of parent record. If record is deleted it will be looked up in
7416
     * an array built before the record was deleted
7417
     *
7418
     * @param string $table Table where record lives/lived
7419
     * @param int $uid Record UID
7420
     * @return int[] Parent UIDs
7421
     */
7422
    protected function getOriginalParentOfRecord($table, $uid)
7423
    {
7424
        if (isset(self::$recordPidsForDeletedRecords[$table][$uid])) {
7425
            return self::$recordPidsForDeletedRecords[$table][$uid];
7426
        }
7427
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

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

7427
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
7428
        return [$parentUid];
7429
    }
7430
7431
    /**
7432
     * Extract entries from TSconfig for a specific table. This will merge specific and default configuration together.
7433
     *
7434
     * @param string $table Table name
7435
     * @param array $TSconfig TSconfig for page
7436
     * @return array TSconfig merged
7437
     */
7438
    public function getTableEntries($table, $TSconfig)
7439
    {
7440
        $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : [];
7441
        $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : [];
7442
        ArrayUtility::mergeRecursiveWithOverrule($dA, $tA);
7443
        return $dA;
7444
    }
7445
7446
    /**
7447
     * Returns the pid of a record from $table with $uid
7448
     *
7449
     * @param string $table Table name
7450
     * @param int $uid Record uid
7451
     * @return int|false PID value (unless the record did not exist in which case FALSE is returned)
7452
     */
7453
    public function getPID($table, $uid)
7454
    {
7455
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7456
        $queryBuilder->getRestrictions()
7457
            ->removeAll();
7458
        $queryBuilder->select('pid')
7459
            ->from($table)
7460
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
7461
        if ($row = $queryBuilder->execute()->fetch()) {
7462
            return $row['pid'];
7463
        }
7464
        return false;
7465
    }
7466
7467
    /**
7468
     * Executing dbAnalysisStore
7469
     * This will save MM relations for new records but is executed after records are created because we need to know the ID of them
7470
     */
7471
    public function dbAnalysisStoreExec()
7472
    {
7473
        foreach ($this->dbAnalysisStore as $action) {
7474
            $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
7475
            if ($id) {
7476
                $action[0]->writeMM($action[1], $id, $action[3]);
7477
            }
7478
        }
7479
    }
7480
7481
    /**
7482
     * Returns array, $CPtable, of pages under the $pid going down to $counter levels.
7483
     * Selecting ONLY pages which the user has read-access to!
7484
     *
7485
     * @param array $CPtable Accumulation of page uid=>pid pairs in branch of $pid
7486
     * @param int $pid Page ID for which to find subpages
7487
     * @param int $counter Number of levels to go down.
7488
     * @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!
7489
     * @return array Return array.
7490
     */
7491
    public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::int_pageTreeInfo" is not in camel caps format
Loading history...
7492
    {
7493
        if ($counter) {
7494
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
7495
            $restrictions = $queryBuilder->getRestrictions()->removeAll();
7496
            $this->addDeleteRestriction($restrictions);
7497
            $queryBuilder
7498
                ->select('uid')
7499
                ->from('pages')
7500
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7501
                ->orderBy('sorting', 'DESC');
7502
            if (!$this->admin) {
7503
                $queryBuilder->andWhere($this->BE_USER->getPagePermsClause(Permission::PAGE_SHOW));
7504
            }
7505
            if ((int)$this->BE_USER->workspace === 0) {
7506
                $queryBuilder->andWhere(
7507
                    $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
7508
                );
7509
            } else {
7510
                $queryBuilder->andWhere($queryBuilder->expr()->in(
7511
                    't3ver_wsid',
7512
                    $queryBuilder->createNamedParameter([0, $this->BE_USER->workspace], Connection::PARAM_INT_ARRAY)
7513
                ));
7514
            }
7515
            $result = $queryBuilder->execute();
7516
7517
            $pages = [];
7518
            while ($row = $result->fetch()) {
7519
                $pages[$row['uid']] = $row;
7520
            }
7521
7522
            // Resolve placeholders of workspace versions
7523
            if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
7524
                $pages = array_reverse(
7525
                    $this->resolveVersionedRecords(
7526
                        'pages',
7527
                        'uid',
7528
                        'sorting',
7529
                        array_keys($pages)
7530
                    ),
7531
                    true
7532
                );
7533
            }
7534
7535
            foreach ($pages as $page) {
7536
                if ($page['uid'] != $rootID) {
7537
                    $CPtable[$page['uid']] = $pid;
7538
                    // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
7539
                    if ($counter - 1) {
7540
                        $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
7541
                    }
7542
                }
7543
            }
7544
        }
7545
        return $CPtable;
7546
    }
7547
7548
    /**
7549
     * List of all tables (those administrators has access to = array_keys of $GLOBALS['TCA'])
7550
     *
7551
     * @return array Array of all TCA table names
7552
     */
7553
    public function compileAdminTables()
7554
    {
7555
        return array_keys($GLOBALS['TCA']);
7556
    }
7557
7558
    /**
7559
     * Checks if any uniqueInPid eval input fields are in the record and if so, they are re-written to be correct.
7560
     *
7561
     * @param string $table Table name
7562
     * @param int $uid Record UID
7563
     */
7564
    public function fixUniqueInPid($table, $uid)
7565
    {
7566
        if (empty($GLOBALS['TCA'][$table])) {
7567
            return;
7568
        }
7569
7570
        $curData = $this->recordInfo($table, $uid, '*');
7571
        $newData = [];
7572
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7573
            if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
7574
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7575
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
7576
                    $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
7577
                    if ((string)$newV !== (string)$curData[$field]) {
7578
                        $newData[$field] = $newV;
7579
                    }
7580
                }
7581
            }
7582
        }
7583
        // IF there are changed fields, then update the database
7584
        if (!empty($newData)) {
7585
            $this->updateDB($table, $uid, $newData);
7586
        }
7587
    }
7588
7589
    /**
7590
     * Checks if any uniqueInSite eval fields are in the record and if so, they are re-written to be correct.
7591
     *
7592
     * @param string $table Table name
7593
     * @param int $uid Record UID
7594
     * @return bool whether the record had to be fixed or not
7595
     */
7596
    protected function fixUniqueInSite(string $table, int $uid): bool
7597
    {
7598
        $curData = $this->recordInfo($table, $uid, '*');
7599
        $workspaceId = $this->BE_USER->workspace;
7600
        $newData = [];
7601
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7602
            if ($conf['config']['type'] === 'slug' && (string)$curData[$field] !== '') {
7603
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7604
                if (in_array('uniqueInSite', $evalCodesArray, true)) {
7605
                    $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $conf['config'], $workspaceId);
7606
                    $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

7606
                    $state = RecordStateFactory::forName($table)->fromArray(/** @scrutinizer ignore-type */ $curData);
Loading history...
7607
                    $newValue = $helper->buildSlugForUniqueInSite($curData[$field], $state);
7608
                    if ((string)$newValue !== (string)$curData[$field]) {
7609
                        $newData[$field] = $newValue;
7610
                    }
7611
                }
7612
            }
7613
        }
7614
        // IF there are changed fields, then update the database
7615
        if (!empty($newData)) {
7616
            $this->updateDB($table, $uid, $newData);
7617
            return true;
7618
        }
7619
        return false;
7620
    }
7621
7622
    /**
7623
     * Check if there are subpages that need an adoption as well
7624
     * @param int $pageId
7625
     */
7626
    protected function fixUniqueInSiteForSubpages(int $pageId)
7627
    {
7628
        // Get ALL subpages to update - read-permissions are respected
7629
        $subPages = $this->int_pageTreeInfo([], $pageId, 99, $pageId);
7630
        // Now fix uniqueInSite for subpages
7631
        foreach ($subPages as $thePageUid => $thePagePid) {
7632
            $recordWasModified = $this->fixUniqueInSite('pages', $thePageUid);
7633
            if ($recordWasModified) {
7634
                // @todo: Add logging and history - but how? we don't know the data that was in the system before
7635
            }
7636
        }
7637
    }
7638
7639
    /**
7640
     * When er record is copied you can specify fields from the previous record which should be copied into the new one
7641
     * 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)
7642
     *
7643
     * @param string $table Table name
7644
     * @param int $uid Record UID
7645
     * @param int $prevUid UID of previous record
7646
     * @param bool $update If set, updates the record
7647
     * @param array $newData Input array. If fields are already specified AND $update is not set, values are not set in output array.
7648
     * @return array Output array (For when the copying operation needs to get the information instead of updating the info)
7649
     */
7650
    public function fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData = [])
7651
    {
7652
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
7653
            $prevData = $this->recordInfo($table, $prevUid, '*');
7654
            $theFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
7655
            foreach ($theFields as $field) {
7656
                if ($GLOBALS['TCA'][$table]['columns'][$field] && ($update || !isset($newData[$field]))) {
7657
                    $newData[$field] = $prevData[$field];
7658
                }
7659
            }
7660
            if ($update && !empty($newData)) {
7661
                $this->updateDB($table, $uid, $newData);
7662
            }
7663
        }
7664
        return $newData;
7665
    }
7666
7667
    /**
7668
     * Casts a reference value. In case MM relations or foreign_field
7669
     * references are used. All other configurations, as well as
7670
     * foreign_table(!) could be stored as comma-separated-values
7671
     * as well. Since the system is not able to determine the default
7672
     * value automatically then, the TCA default value is used if
7673
     * it has been defined.
7674
     *
7675
     * @param int|string $value The value to be casted (e.g. '', '0', '1,2,3')
7676
     * @param array $configuration The TCA configuration of the accordant field
7677
     * @return int|string
7678
     */
7679
    protected function castReferenceValue($value, array $configuration)
7680
    {
7681
        if ((string)$value !== '') {
7682
            return $value;
7683
        }
7684
7685
        if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
7686
            return 0;
7687
        }
7688
7689
        if (array_key_exists('default', $configuration)) {
7690
            return $configuration['default'];
7691
        }
7692
7693
        return $value;
7694
    }
7695
7696
    /**
7697
     * Returns TRUE if the TCA/columns field type is a DB reference field
7698
     *
7699
     * @param array $conf Config array for TCA/columns field
7700
     * @return bool TRUE if DB reference field (group/db or select with foreign-table)
7701
     */
7702
    public function isReferenceField($conf)
7703
    {
7704
        return $conf['type'] === 'group' && $conf['internal_type'] === 'db' || $conf['type'] === 'select' && $conf['foreign_table'];
7705
    }
7706
7707
    /**
7708
     * Returns the subtype as a string of an inline field.
7709
     * If it's not an inline field at all, it returns FALSE.
7710
     *
7711
     * @param array $conf Config array for TCA/columns field
7712
     * @return string|bool string Inline subtype (field|mm|list), boolean: FALSE
7713
     */
7714
    public function getInlineFieldType($conf)
7715
    {
7716
        if ($conf['type'] !== 'inline' || !$conf['foreign_table']) {
7717
            return false;
7718
        }
7719
        if ($conf['foreign_field']) {
7720
            // The reference to the parent is stored in a pointer field in the child record
7721
            return 'field';
7722
        }
7723
        if ($conf['MM']) {
7724
            // Regular MM intermediate table is used to store data
7725
            return 'mm';
7726
        }
7727
        // An item list (separated by comma) is stored (like select type is doing)
7728
        return 'list';
7729
    }
7730
7731
    /**
7732
     * Get modified header for a copied record
7733
     *
7734
     * @param string $table Table name
7735
     * @param int $pid PID value in which other records to test might be
7736
     * @param string $field Field name to get header value for.
7737
     * @param string $value Current field value
7738
     * @param int $count Counter (number of recursions)
7739
     * @param string $prevTitle Previous title we checked for (in previous recursion)
7740
     * @return string The field value, possibly appended with a "copy label
7741
     */
7742
    public function getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
7743
    {
7744
        // Set title value to check for:
7745
        $checkTitle = $value;
7746
        if ($count > 0) {
7747
            $checkTitle = $value . rtrim(' ' . sprintf($this->prependLabel($table), $count));
7748
        }
7749
        // Do check:
7750
        if ($prevTitle != $checkTitle || $count < 100) {
7751
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7752
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7753
            $rowCount = $queryBuilder
7754
                ->count('uid')
7755
                ->from($table)
7756
                ->where(
7757
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
7758
                    $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($checkTitle, \PDO::PARAM_STR))
7759
                )
7760
                ->execute()
7761
                ->fetchColumn(0);
7762
            if ($rowCount) {
7763
                return $this->getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
7764
            }
7765
        }
7766
        // Default is to just return the current input title if no other was returned before:
7767
        return $checkTitle;
7768
    }
7769
7770
    /**
7771
     * Return "copy" label for a table. Although the name is "prepend" it actually APPENDs the label (after ...)
7772
     *
7773
     * @param string $table Table name
7774
     * @return string Label to append, containing "%s" for the number
7775
     * @see getCopyHeader()
7776
     */
7777
    public function prependLabel($table)
7778
    {
7779
        return $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
7780
    }
7781
7782
    /**
7783
     * Get the final pid based on $table and $pid ($destPid type... pos/neg)
7784
     *
7785
     * @param string $table Table name
7786
     * @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!
7787
     * @return int
7788
     */
7789
    public function resolvePid($table, $pid)
7790
    {
7791
        $pid = (int)$pid;
7792
        if ($pid < 0) {
7793
            $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7794
            $query->getRestrictions()
7795
                ->removeAll();
7796
            $row = $query
7797
                ->select('pid')
7798
                ->from($table)
7799
                ->where($query->expr()->eq('uid', $query->createNamedParameter(abs($pid), \PDO::PARAM_INT)))
7800
                ->execute()
7801
                ->fetch();
7802
            // Look, if the record UID happens to be an offline record. If so, find its live version.
7803
            if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, abs($pid), 'pid')) {
0 ignored issues
show
Bug introduced by
It seems like abs($pid) can also be of type double; however, parameter $uid of TYPO3\CMS\Backend\Utilit...etLiveVersionOfRecord() 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

7803
            if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, /** @scrutinizer ignore-type */ abs($pid), 'pid')) {
Loading history...
7804
                $row = $lookForLiveVersion;
7805
            }
7806
            $pid = (int)$row['pid'];
7807
        }
7808
        return $pid;
7809
    }
7810
7811
    /**
7812
     * Removes the prependAtCopy prefix on values
7813
     *
7814
     * @param string $table Table name
7815
     * @param string $value The value to fix
7816
     * @return string Clean name
7817
     */
7818
    public function clearPrefixFromValue($table, $value)
7819
    {
7820
        $regex = '/\s' . sprintf(preg_quote($this->prependLabel($table)), '[0-9]*') . '$/';
7821
        return @preg_replace($regex, '', $value);
7822
    }
7823
7824
    /**
7825
     * Check if there are records from tables on the pages to be deleted which the current user is not allowed to
7826
     *
7827
     * @param int[] $pageIds IDs of pages which should be checked
7828
     * @return bool Return TRUE, if permission granted
7829
     * @see canDeletePage()
7830
     */
7831
    protected function checkForRecordsFromDisallowedTables(array $pageIds)
7832
    {
7833
        if ($this->admin) {
7834
            return true;
7835
        }
7836
7837
        if (!empty($pageIds)) {
7838
            $tableNames = $this->compileAdminTables();
7839
            foreach ($tableNames as $table) {
7840
                $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7841
                $query->getRestrictions()
7842
                    ->removeAll()
7843
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7844
                $count = $query->count('uid')
7845
                    ->from($table)
7846
                    ->where($query->expr()->in(
7847
                        'pid',
7848
                        $query->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
7849
                    ))
7850
                    ->execute()
7851
                    ->fetchColumn(0);
7852
                if ($count && ($this->tableReadOnly($table) || !$this->checkModifyAccessList($table))) {
7853
                    return false;
7854
                }
7855
            }
7856
        }
7857
        return true;
7858
    }
7859
7860
    /**
7861
     * Determine if a record was copied or if a record is the result of a copy action.
7862
     *
7863
     * @param string $table The tablename of the record
7864
     * @param int $uid The uid of the record
7865
     * @return bool Returns TRUE if the record is copied or is the result of a copy action
7866
     */
7867
    public function isRecordCopied($table, $uid)
7868
    {
7869
        // If the record was copied:
7870
        if (isset($this->copyMappingArray[$table][$uid])) {
7871
            return true;
7872
        }
7873
        if (isset($this->copyMappingArray[$table]) && in_array($uid, array_values($this->copyMappingArray[$table]))) {
7874
            return true;
7875
        }
7876
        return false;
7877
    }
7878
7879
    /******************************
7880
     *
7881
     * Clearing cache
7882
     *
7883
     ******************************/
7884
7885
    /**
7886
     * Clearing the cache based on a page being updated
7887
     * If the $table is 'pages' then cache is cleared for all pages on the same level (and subsequent?)
7888
     * Else just clear the cache for the parent page of the record.
7889
     *
7890
     * @param string $table Table name of record that was just updated.
7891
     * @param int $uid UID of updated / inserted record
7892
     * @param int $pid REAL PID of page of a deleted/moved record to get TSconfig in ClearCache.
7893
     * @internal This method is not meant to be called directly but only from the core itself or from hooks
7894
     */
7895
    public function registerRecordIdForPageCacheClearing($table, $uid, $pid = null)
7896
    {
7897
        if (!is_array(static::$recordsToClearCacheFor[$table])) {
7898
            static::$recordsToClearCacheFor[$table] = [];
7899
        }
7900
        static::$recordsToClearCacheFor[$table][] = (int)$uid;
7901
        if ($pid !== null) {
7902
            if (!is_array(static::$recordPidsForDeletedRecords[$table])) {
7903
                static::$recordPidsForDeletedRecords[$table] = [];
7904
            }
7905
            static::$recordPidsForDeletedRecords[$table][$uid][] = (int)$pid;
7906
        }
7907
    }
7908
7909
    /**
7910
     * Do the actual clear cache
7911
     */
7912
    protected function processClearCacheQueue()
7913
    {
7914
        $tagsToClear = [];
7915
        $clearCacheCommands = [];
7916
7917
        foreach (static::$recordsToClearCacheFor as $table => $uids) {
7918
            foreach (array_unique($uids) as $uid) {
7919
                if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
7920
                    return;
7921
                }
7922
                // For move commands we may get more then 1 parent.
7923
                $pageUids = $this->getOriginalParentOfRecord($table, $uid);
7924
                foreach ($pageUids as $originalParent) {
7925
                    [$tagsToClearFromPrepare, $clearCacheCommandsFromPrepare]
7926
                        = $this->prepareCacheFlush($table, $uid, $originalParent);
7927
                    $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
7928
                    $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
7929
                }
7930
            }
7931
        }
7932
7933
        /** @var CacheManager $cacheManager */
7934
        $cacheManager = $this->getCacheManager();
7935
        $cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear));
7936
7937
        // Filter duplicate cache commands from cacheQueue
7938
        $clearCacheCommands = array_unique($clearCacheCommands);
7939
        // Execute collected clear cache commands from page TSConfig
7940
        foreach ($clearCacheCommands as $command) {
7941
            $this->clear_cacheCmd($command);
7942
        }
7943
7944
        // Reset the cache clearing array
7945
        static::$recordsToClearCacheFor = [];
7946
7947
        // Reset the original pid array
7948
        static::$recordPidsForDeletedRecords = [];
7949
    }
7950
7951
    /**
7952
     * Prepare the cache clearing
7953
     *
7954
     * @param string $table Table name of record that needs to be cleared
7955
     * @param int $uid UID of record for which the cache needs to be cleared
7956
     * @param int $pid Original pid of the page of the record which the cache needs to be cleared
7957
     * @return array Array with tagsToClear and clearCacheCommands
7958
     * @internal This function is internal only it may be changed/removed also in minor version numbers.
7959
     */
7960
    protected function prepareCacheFlush($table, $uid, $pid)
7961
    {
7962
        $tagsToClear = [];
7963
        $clearCacheCommands = [];
7964
        $pageUid = 0;
7965
        // Get Page TSconfig relevant:
7966
        $TSConfig = BackendUtility::getPagesTSconfig($pid)['TCEMAIN.'] ?? [];
7967
        if (empty($TSConfig['clearCache_disable'])) {
7968
            $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
7969
            // If table is "pages":
7970
            $pageIdsThatNeedCacheFlush = [];
7971
            if ($table === 'pages') {
7972
                // Find out if the record is a get the original page
7973
                $pageUid = $this->getDefaultLanguagePageId($uid);
7974
7975
                // Builds list of pages on the SAME level as this page (siblings)
7976
                $queryBuilder = $connectionPool->getQueryBuilderForTable('pages');
7977
                $queryBuilder->getRestrictions()
7978
                    ->removeAll()
7979
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7980
                $siblings = $queryBuilder
7981
                    ->select('A.pid AS pid', 'B.uid AS uid')
7982
                    ->from('pages', 'A')
7983
                    ->from('pages', 'B')
7984
                    ->where(
7985
                        $queryBuilder->expr()->eq('A.uid', $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)),
7986
                        $queryBuilder->expr()->eq('B.pid', $queryBuilder->quoteIdentifier('A.pid')),
7987
                        $queryBuilder->expr()->gte('A.pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
7988
                    )
7989
                    ->execute();
7990
7991
                $parentPageId = 0;
7992
                while ($row_tmp = $siblings->fetch()) {
7993
                    $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
7994
                    $parentPageId = (int)$row_tmp['pid'];
7995
                    // Add children as well:
7996
                    if ($TSConfig['clearCache_pageSiblingChildren']) {
7997
                        $siblingChildrenQuery = $connectionPool->getQueryBuilderForTable('pages');
7998
                        $siblingChildrenQuery->getRestrictions()
7999
                            ->removeAll()
8000
                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8001
                        $siblingChildren = $siblingChildrenQuery
8002
                            ->select('uid')
8003
                            ->from('pages')
8004
                            ->where($siblingChildrenQuery->expr()->eq(
8005
                                'pid',
8006
                                $siblingChildrenQuery->createNamedParameter($row_tmp['uid'], \PDO::PARAM_INT)
8007
                            ))
8008
                            ->execute();
8009
                        while ($row_tmp2 = $siblingChildren->fetch()) {
8010
                            $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
8011
                        }
8012
                    }
8013
                }
8014
                // Finally, add the parent page as well when clearing a specific page
8015
                if ($parentPageId > 0) {
8016
                    $pageIdsThatNeedCacheFlush[] = $parentPageId;
8017
                }
8018
                // Add grand-parent as well if configured
8019
                if ($TSConfig['clearCache_pageGrandParent']) {
8020
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8021
                    $parentQuery->getRestrictions()
8022
                        ->removeAll()
8023
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8024
                    $row_tmp = $parentQuery
8025
                        ->select('pid')
8026
                        ->from('pages')
8027
                        ->where($parentQuery->expr()->eq(
8028
                            'uid',
8029
                            $parentQuery->createNamedParameter($parentPageId, \PDO::PARAM_INT)
8030
                        ))
8031
                        ->execute()
8032
                        ->fetch();
8033
                    if (!empty($row_tmp)) {
8034
                        $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
8035
                    }
8036
                }
8037
            } else {
8038
                // For other tables than "pages", delete cache for the records "parent page".
8039
                $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
8040
                // Add the parent page as well
8041
                if ($TSConfig['clearCache_pageGrandParent']) {
8042
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8043
                    $parentQuery->getRestrictions()
8044
                        ->removeAll()
8045
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8046
                    $parentPageRecord = $parentQuery
8047
                        ->select('pid')
8048
                        ->from('pages')
8049
                        ->where($parentQuery->expr()->eq(
8050
                            'uid',
8051
                            $parentQuery->createNamedParameter($pageUid, \PDO::PARAM_INT)
8052
                        ))
8053
                        ->execute()
8054
                        ->fetch();
8055
                    if (!empty($parentPageRecord)) {
8056
                        $pageIdsThatNeedCacheFlush[] = (int)$parentPageRecord['pid'];
8057
                    }
8058
                }
8059
            }
8060
            // Call pre-processing function for clearing of cache for page ids:
8061
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8062
                $_params = ['pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()'];
8063
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8064
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8065
            }
8066
            // Delete cache for selected pages:
8067
            foreach ($pageIdsThatNeedCacheFlush as $pageId) {
8068
                // Workspaces always use "-1" as the page id which do not
8069
                // point to real pages and caches at all. Flushing caches for
8070
                // those records does not make sense and decreases performance
8071
                if ($pageId >= 0) {
8072
                    $tagsToClear['pageId_' . $pageId] = true;
8073
                }
8074
            }
8075
            // Queue delete cache for current table and record
8076
            $tagsToClear[$table] = true;
8077
            $tagsToClear[$table . '_' . $uid] = true;
8078
        }
8079
        // Clear cache for pages entered in TSconfig:
8080
        if (!empty($TSConfig['clearCacheCmd'])) {
8081
            $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
8082
            $clearCacheCommands = array_unique($commands);
8083
        }
8084
        // Call post processing function for clear-cache:
8085
        $_params = ['table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig];
8086
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8087
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8088
        }
8089
        return [
8090
            $tagsToClear,
8091
            $clearCacheCommands
8092
        ];
8093
    }
8094
8095
    /**
8096
     * Clears the cache based on the command $cacheCmd.
8097
     *
8098
     * $cacheCmd='pages'
8099
     * Clears cache for all pages and page-based caches inside the cache manager.
8100
     * Requires admin-flag to be set for BE_USER.
8101
     *
8102
     * $cacheCmd='all'
8103
     * Clears all cache_tables. This is necessary if templates are updated.
8104
     * Requires admin-flag to be set for BE_USER.
8105
     *
8106
     * The following cache_* are intentionally not cleared by 'all'
8107
     *
8108
     * - imagesizes:	Clearing this table would cause a lot of unneeded
8109
     * Imagemagick calls because the size information has
8110
     * to be fetched again after clearing.
8111
     * - all caches inside the cache manager that are inside the group "system"
8112
     * - they are only needed to build up the core system and templates,
8113
     *   use "temp_cached" or "system" to do that
8114
     *
8115
     * $cacheCmd=[integer]
8116
     * Clears cache for the page pointed to by $cacheCmd (an integer).
8117
     *
8118
     * $cacheCmd='cacheTag:[string]'
8119
     * Flush page and pagesection cache by given tag
8120
     *
8121
     * $cacheCmd='cacheId:[string]'
8122
     * Removes cache identifier from page and page section cache
8123
     *
8124
     * Can call a list of post processing functions as defined in
8125
     * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc']
8126
     * (numeric array with values being the function references, called by
8127
     * GeneralUtility::callUserFunction()).
8128
     *
8129
     *
8130
     * @param string $cacheCmd The cache command, see above description
8131
     */
8132
    public function clear_cacheCmd($cacheCmd)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::clear_cacheCmd" is not in camel caps format
Loading history...
8133
    {
8134
        if (is_object($this->BE_USER)) {
8135
            $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]);
8136
        }
8137
        $userTsConfig = $this->BE_USER->getTSConfig();
8138
        switch (strtolower($cacheCmd)) {
8139
            case 'pages':
8140
                if ($this->admin || $userTsConfig['options.']['clearCache.']['pages'] ?? false) {
8141
                    $this->getCacheManager()->flushCachesInGroup('pages');
8142
                }
8143
                break;
8144
            case 'all':
8145
                // allow to clear all caches if the TS config option is enabled or the option is not explicitly
8146
                // disabled for admins (which could clear all caches by default). The latter option is useful
8147
                // for big production sites where it should be possible to restrict the cache clearing for some admins.
8148
                if ($userTsConfig['options.']['clearCache.']['all'] ?? false
8149
                    || ($this->admin && (bool)($userTsConfig['options.']['clearCache.']['all'] ?? true))
8150
                ) {
8151
                    $this->getCacheManager()->flushCaches();
8152
                    GeneralUtility::makeInstance(ConnectionPool::class)
8153
                        ->getConnectionForTable('cache_treelist')
8154
                        ->truncate('cache_treelist');
8155
8156
                    // Delete Opcode Cache
8157
                    GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
8158
                }
8159
                break;
8160
        }
8161
8162
        $tagsToFlush = [];
8163
        // Clear cache for a page ID!
8164
        if (MathUtility::canBeInterpretedAsInteger($cacheCmd)) {
8165
            $list_cache = [$cacheCmd];
8166
            // Call pre-processing function for clearing of cache for page ids:
8167
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8168
                $_params = ['pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()'];
8169
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8170
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8171
            }
8172
            // Delete cache for selected pages:
8173
            if (is_array($list_cache)) {
0 ignored issues
show
introduced by
The condition is_array($list_cache) is always true.
Loading history...
8174
                foreach ($list_cache as $pageId) {
8175
                    $tagsToFlush[] = 'pageId_' . (int)$pageId;
8176
                }
8177
            }
8178
        }
8179
        // flush cache by tag
8180
        if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
8181
            $cacheTag = substr($cacheCmd, 9);
8182
            $tagsToFlush[] = $cacheTag;
8183
        }
8184
        // process caching framework operations
8185
        if (!empty($tagsToFlush)) {
8186
            $this->getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);
8187
        }
8188
8189
        // Call post processing function for clear-cache:
8190
        $_params = ['cacheCmd' => strtolower($cacheCmd)];
8191
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8192
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8193
        }
8194
    }
8195
8196
    /*****************************
8197
     *
8198
     * Logging
8199
     *
8200
     *****************************/
8201
    /**
8202
     * Logging actions from DataHandler
8203
     *
8204
     * @param string $table Table name the log entry is concerned with. Blank if NA
8205
     * @param int $recuid Record UID. Zero if NA
8206
     * @param int $action Action number: 0=No category, 1=new record, 2=update record, 3= delete record, 4= move record, 5= Check/evaluate
8207
     * @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
8208
     * @param int $error The severity: 0 = message, 1 = error, 2 = System Error, 3 = security notice (admin), 4 warning
8209
     * @param string $details Default error message in english
8210
     * @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
8211
     * @param array $data Array with special information that may go into $details by '%s' marks / sprintf() when the log is shown
8212
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
8213
     * @param string $NEWid NEW id for new records
8214
     * @return int Log entry UID (0 if no log entry was written or logging is disabled)
8215
     * @see \TYPO3\CMS\Core\SysLog\Action\Database for all available values of argument $action
8216
     * @see \TYPO3\CMS\Core\SysLog\Error for all available values of argument $error
8217
     */
8218
    public function log($table, $recuid, $action, $recpid, $error, $details, $details_nr = -1, $data = [], $event_pid = -1, $NEWid = '')
8219
    {
8220
        if (!$this->enableLogging) {
8221
            return 0;
8222
        }
8223
        // Type value for DataHandler
8224
        if (!$this->storeLogMessages) {
8225
            $details = '';
8226
        }
8227
        if ($error > 0) {
8228
            $detailMessage = $details;
8229
            if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
8230
                $detailMessage = vsprintf($details, $data);
8231
            }
8232
            $this->errorLog[] = '[' . SystemLogType::DB . '.' . $action . '.' . $details_nr . ']: ' . $detailMessage;
8233
        }
8234
        return $this->BE_USER->writelog(SystemLogType::DB, $action, $error, $details_nr, $details, $data, $table, $recuid, $recpid, $event_pid, $NEWid);
8235
    }
8236
8237
    /**
8238
     * Simple logging function meant to be used when logging messages is not yet fixed.
8239
     *
8240
     * @param string $message Message string
8241
     * @param int $error Error code, see log()
8242
     * @return int Log entry UID
8243
     * @see log()
8244
     */
8245
    public function newlog($message, $error = SystemLogErrorClassification::MESSAGE)
8246
    {
8247
        return $this->log('', 0, SystemLogGenericAction::UNDEFINED, 0, $error, $message, -1);
8248
    }
8249
8250
    /**
8251
     * Print log error messages from the operations of this script instance
8252
     */
8253
    public function printLogErrorMessages()
8254
    {
8255
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
8256
        $queryBuilder->getRestrictions()->removeAll();
8257
        $result = $queryBuilder
8258
            ->select('*')
8259
            ->from('sys_log')
8260
            ->where(
8261
                $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
8262
                $queryBuilder->expr()->lt('action', $queryBuilder->createNamedParameter(256, \PDO::PARAM_INT)),
8263
                $queryBuilder->expr()->eq(
8264
                    'userid',
8265
                    $queryBuilder->createNamedParameter($this->BE_USER->user['uid'], \PDO::PARAM_INT)
8266
                ),
8267
                $queryBuilder->expr()->eq(
8268
                    'tstamp',
8269
                    $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
8270
                ),
8271
                $queryBuilder->expr()->neq('error', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8272
            )
8273
            ->execute();
8274
8275
        while ($row = $result->fetch()) {
8276
            $log_data = unserialize($row['log_data']);
8277
            $msg = $row['error'] . ': ' . sprintf($row['details'], $log_data[0], $log_data[1], $log_data[2], $log_data[3], $log_data[4]);
8278
            /** @var FlashMessage $flashMessage */
8279
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', $row['error'] === 4 ? FlashMessage::WARNING : FlashMessage::ERROR, true);
8280
            /** @var FlashMessageService $flashMessageService */
8281
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
8282
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
8283
            $defaultFlashMessageQueue->enqueue($flashMessage);
8284
        }
8285
    }
8286
8287
    /*****************************
8288
     *
8289
     * Internal (do not use outside Core!)
8290
     *
8291
     *****************************/
8292
8293
    /**
8294
     * Find out if the record is a get the original page
8295
     *
8296
     * @param int $pageId the page UID (can be the default page record, or a page translation record ID)
8297
     * @return int the page UID of the default page record
8298
     */
8299
    protected function getDefaultLanguagePageId(int $pageId): int
8300
    {
8301
        $localizationParentFieldName = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
8302
        $row = $this->recordInfo('pages', $pageId, $localizationParentFieldName);
8303
        $localizationParent = (int)$row[$localizationParentFieldName];
8304
        if ($localizationParent > 0) {
8305
            return $localizationParent;
8306
        }
8307
        return $pageId;
8308
    }
8309
8310
    /**
8311
     * Preprocesses field array based on field type. Some fields must be adjusted
8312
     * before going to database. This is done on the copy of the field array because
8313
     * original values are used in remap action later.
8314
     *
8315
     * @param string $table	Table name
8316
     * @param array $fieldArray	Field array to check
8317
     * @return array Updated field array
8318
     */
8319
    public function insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandler::insertUpdateDB_preprocessBasedOnFieldType" is not in camel caps format
Loading history...
8320
    {
8321
        $result = $fieldArray;
8322
        foreach ($fieldArray as $field => $value) {
8323
            if (!MathUtility::canBeInterpretedAsInteger($value)
8324
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'inline'
8325
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_field']) {
8326
                $result[$field] = count(GeneralUtility::trimExplode(',', $value, true));
8327
            }
8328
        }
8329
        return $result;
8330
    }
8331
8332
    /**
8333
     * Determines whether a particular record has been deleted
8334
     * using DataHandler::deleteRecord() in this instance.
8335
     *
8336
     * @param string $tableName
8337
     * @param string $uid
8338
     * @return bool
8339
     */
8340
    public function hasDeletedRecord($tableName, $uid)
8341
    {
8342
        return
8343
            !empty($this->deletedRecords[$tableName])
8344
            && in_array($uid, $this->deletedRecords[$tableName])
8345
        ;
0 ignored issues
show
Coding Style introduced by
Space found before semicolon; expected ");" but found ")
;"
Loading history...
8346
    }
8347
8348
    /**
8349
     * Gets the automatically versionized id of a record.
8350
     *
8351
     * @param string $table Name of the table
8352
     * @param int $id Uid of the record
8353
     * @return int
8354
     */
8355
    public function getAutoVersionId($table, $id)
8356
    {
8357
        $result = null;
8358
        if (isset($this->autoVersionIdMap[$table][$id])) {
8359
            $result = $this->autoVersionIdMap[$table][$id];
8360
        }
8361
        return $result;
8362
    }
8363
8364
    /**
8365
     * Overlays the automatically versionized id of a record.
8366
     *
8367
     * @param string $table Name of the table
8368
     * @param int $id Uid of the record
8369
     * @return int
8370
     */
8371
    protected function overlayAutoVersionId($table, $id)
8372
    {
8373
        $autoVersionId = $this->getAutoVersionId($table, $id);
8374
        if ($autoVersionId !== null) {
0 ignored issues
show
introduced by
The condition $autoVersionId !== null is always true.
Loading history...
8375
            $id = $autoVersionId;
8376
        }
8377
        return $id;
8378
    }
8379
8380
    /**
8381
     * Adds new values to the remapStackChildIds array.
8382
     *
8383
     * @param array $idValues uid values
8384
     */
8385
    protected function addNewValuesToRemapStackChildIds(array $idValues)
8386
    {
8387
        foreach ($idValues as $idValue) {
8388
            if (strpos($idValue, 'NEW') === 0) {
8389
                $this->remapStackChildIds[$idValue] = true;
8390
            }
8391
        }
8392
    }
8393
8394
    /**
8395
     * Resolves versioned records for the current workspace scope.
8396
     * Delete placeholders and move placeholders are substituted and removed.
8397
     *
8398
     * @param string $tableName Name of the table to be processed
8399
     * @param string $fieldNames List of the field names to be fetched
8400
     * @param string $sortingField Name of the sorting field to be used
8401
     * @param array $liveIds Flat array of (live) record ids
8402
     * @return array
8403
     */
8404
    protected function resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
8405
    {
8406
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
8407
            ->getConnectionForTable($tableName);
8408
        $sortingStatement = !empty($sortingField)
8409
            ? [$connection->quoteIdentifier($sortingField)]
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
8410
            : null;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
8411
        /** @var PlainDataResolver $resolver */
8412
        $resolver = GeneralUtility::makeInstance(
8413
            PlainDataResolver::class,
8414
            $tableName,
8415
            $liveIds,
8416
            $sortingStatement
8417
        );
8418
8419
        $resolver->setWorkspaceId($this->BE_USER->workspace);
8420
        $resolver->setKeepDeletePlaceholder(false);
8421
        $resolver->setKeepMovePlaceholder(false);
8422
        $resolver->setKeepLiveIds(true);
8423
        $recordIds = $resolver->get();
8424
8425
        $records = [];
8426
        foreach ($recordIds as $recordId) {
8427
            $records[$recordId] = BackendUtility::getRecord($tableName, $recordId, $fieldNames);
8428
        }
8429
8430
        return $records;
8431
    }
8432
8433
    /**
8434
     * Gets the outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler
8435
     * Since \TYPO3\CMS\Core\DataHandling\DataHandler can create nested objects of itself,
8436
     * this method helps to determine the first (= outer most) one.
8437
     *
8438
     * @return DataHandler
8439
     */
8440
    protected function getOuterMostInstance()
8441
    {
8442
        if (!isset($this->outerMostInstance)) {
8443
            $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
8444
            foreach ($stack as $stackItem) {
8445
                if (isset($stackItem['object']) && $stackItem['object'] instanceof self) {
8446
                    $this->outerMostInstance = $stackItem['object'];
8447
                    break;
8448
                }
8449
            }
8450
        }
8451
        return $this->outerMostInstance;
8452
    }
8453
8454
    /**
8455
     * Determines whether the this object is the outer most instance of itself
8456
     * Since DataHandler can create nested objects of itself,
8457
     * this method helps to determine the first (= outer most) one.
8458
     *
8459
     * @return bool
8460
     */
8461
    public function isOuterMostInstance()
8462
    {
8463
        return $this->getOuterMostInstance() === $this;
8464
    }
8465
8466
    /**
8467
     * Gets an instance of the runtime cache.
8468
     *
8469
     * @return FrontendInterface
8470
     */
8471
    protected function getRuntimeCache()
8472
    {
8473
        return $this->getCacheManager()->getCache('runtime');
8474
    }
8475
8476
    /**
8477
     * Determines nested element calls.
8478
     *
8479
     * @param string $table Name of the table
8480
     * @param int $id Uid of the record
8481
     * @param string $identifier Name of the action to be checked
8482
     * @return bool
8483
     */
8484
    protected function isNestedElementCallRegistered($table, $id, $identifier)
8485
    {
8486
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8487
        return isset($nestedElementCalls[$identifier][$table][$id]);
8488
    }
8489
8490
    /**
8491
     * Registers nested elements calls.
8492
     * This is used to track nested calls (e.g. for following m:n relations).
8493
     *
8494
     * @param string $table Name of the table
8495
     * @param int $id Uid of the record
8496
     * @param string $identifier Name of the action to be tracked
8497
     */
8498
    protected function registerNestedElementCall($table, $id, $identifier)
8499
    {
8500
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8501
        $nestedElementCalls[$identifier][$table][$id] = true;
8502
        $this->runtimeCache->set($this->cachePrefixNestedElementCalls, $nestedElementCalls);
8503
    }
8504
8505
    /**
8506
     * Resets the nested element calls.
8507
     */
8508
    protected function resetNestedElementCalls()
8509
    {
8510
        $this->runtimeCache->remove($this->cachePrefixNestedElementCalls);
8511
    }
8512
8513
    /**
8514
     * Determines whether an element was registered to be deleted in the registry.
8515
     *
8516
     * @param string $table Name of the table
8517
     * @param int $id Uid of the record
8518
     * @return bool
8519
     * @see registerElementsToBeDeleted
8520
     * @see resetElementsToBeDeleted
8521
     * @see copyRecord_raw
8522
     * @see versionizeRecord
8523
     */
8524
    protected function isElementToBeDeleted($table, $id)
8525
    {
8526
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8527
        return isset($elementsToBeDeleted[$table][$id]);
8528
    }
8529
8530
    /**
8531
     * Registers elements to be deleted in the registry.
8532
     *
8533
     * @see process_datamap
8534
     */
8535
    protected function registerElementsToBeDeleted()
8536
    {
8537
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8538
        $this->runtimeCache->set('core-datahandler-elementsToBeDeleted', array_merge($elementsToBeDeleted, $this->getCommandMapElements('delete')));
8539
    }
8540
8541
    /**
8542
     * Resets the elements to be deleted in the registry.
8543
     *
8544
     * @see process_datamap
8545
     */
8546
    protected function resetElementsToBeDeleted()
8547
    {
8548
        $this->runtimeCache->remove('core-datahandler-elementsToBeDeleted');
8549
    }
8550
8551
    /**
8552
     * Unsets elements (e.g. of the data map) that shall be deleted.
8553
     * This avoids to modify records that will be deleted later on.
8554
     *
8555
     * @param array $elements Elements to be modified
8556
     * @return array
8557
     */
8558
    protected function unsetElementsToBeDeleted(array $elements)
8559
    {
8560
        $elements = ArrayUtility::arrayDiffAssocRecursive($elements, $this->getCommandMapElements('delete'));
8561
        foreach ($elements as $key => $value) {
8562
            if (empty($value)) {
8563
                unset($elements[$key]);
8564
            }
8565
        }
8566
        return $elements;
8567
    }
8568
8569
    /**
8570
     * Gets elements of the command map that match a particular command.
8571
     *
8572
     * @param string $needle The command to be matched
8573
     * @return array
8574
     */
8575
    protected function getCommandMapElements($needle)
8576
    {
8577
        $elements = [];
8578
        foreach ($this->cmdmap as $tableName => $idArray) {
8579
            foreach ($idArray as $id => $commandArray) {
8580
                foreach ($commandArray as $command => $value) {
8581
                    if ($value && $command == $needle) {
8582
                        $elements[$tableName][$id] = true;
8583
                    }
8584
                }
8585
            }
8586
        }
8587
        return $elements;
8588
    }
8589
8590
    /**
8591
     * Controls active elements and sets NULL values if not active.
8592
     * Datamap is modified accordant to submitted control values.
8593
     */
8594
    protected function controlActiveElements()
8595
    {
8596
        if (!empty($this->control['active'])) {
8597
            $this->setNullValues(
8598
                $this->control['active'],
8599
                $this->datamap
8600
            );
8601
        }
8602
    }
8603
8604
    /**
8605
     * Sets NULL values in haystack array.
8606
     * The general behaviour in the user interface is to enable/activate fields.
8607
     * Thus, this method uses NULL as value to be stored if a field is not active.
8608
     *
8609
     * @param array $active hierarchical array with active elements
8610
     * @param array $haystack hierarchical array with haystack to be modified
8611
     */
8612
    protected function setNullValues(array $active, array &$haystack)
8613
    {
8614
        foreach ($active as $key => $value) {
8615
            // Nested data is processes recursively
8616
            if (is_array($value)) {
8617
                $this->setNullValues(
8618
                    $value,
8619
                    $haystack[$key]
8620
                );
8621
            } elseif ($value == 0) {
8622
                // Field has not been activated in the user interface,
8623
                // thus a NULL value shall be stored in the database
8624
                $haystack[$key] = null;
8625
            }
8626
        }
8627
    }
8628
8629
    /**
8630
     * @param CorrelationId $correlationId
8631
     */
8632
    public function setCorrelationId(CorrelationId $correlationId): void
8633
    {
8634
        $this->correlationId = $correlationId;
8635
    }
8636
8637
    /**
8638
     * @return CorrelationId|null
8639
     */
8640
    public function getCorrelationId(): ?CorrelationId
8641
    {
8642
        return $this->correlationId;
8643
    }
8644
8645
    /**
8646
     * Entry point to post process a database insert. Currently bails early unless a UID has been forced
8647
     * and the database platform is not MySQL.
8648
     *
8649
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8650
     * @param string $tableName
8651
     * @param int $suggestedUid
8652
     * @return int
8653
     */
8654
    protected function postProcessDatabaseInsert(Connection $connection, string $tableName, int $suggestedUid): int
8655
    {
8656
        if ($suggestedUid !== 0 && $connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
8657
            $this->postProcessPostgresqlInsert($connection, $tableName);
8658
            // The last inserted id on postgresql is actually the last value generated by the sequence.
8659
            // On a forced UID insert this might not be the actual value or the sequence might not even
8660
            // have generated a value yet.
8661
            // Return the actual ID we forced on insert as a surrogate.
8662
            return $suggestedUid;
8663
        }
8664
        if ($connection->getDatabasePlatform() instanceof SQLServerPlatform) {
8665
            return $this->postProcessSqlServerInsert($connection, $tableName);
8666
        }
8667
        $id = $connection->lastInsertId($tableName);
8668
        return (int)$id;
8669
    }
8670
8671
    /**
8672
     * Get the last insert ID from sql server
8673
     *
8674
     * - first checks whether doctrine might be able to fetch the ID from the
8675
     * sequence table
8676
     * - if that does not succeed it manually selects the current IDENTITY value
8677
     * from a table
8678
     * - returns 0 if both fail
8679
     *
8680
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8681
     * @param string $tableName
8682
     * @return int
8683
     * @throws \Doctrine\DBAL\DBALException
8684
     */
8685
    protected function postProcessSqlServerInsert(Connection $connection, string $tableName): int
8686
    {
8687
        $id = $connection->lastInsertId($tableName);
8688
        if (!((int)$id > 0)) {
8689
            $table = $connection->quoteIdentifier($tableName);
8690
            $result = $connection->executeQuery('SELECT IDENT_CURRENT(\'' . $table . '\') AS id')->fetch();
8691
            if (isset($result['id']) && $result['id'] > 0) {
8692
                $id = $result['id'];
8693
            }
8694
        }
8695
        return (int)$id;
8696
    }
8697
8698
    /**
8699
     * PostgreSQL works with sequences for auto increment columns. A sequence is not updated when a value is
8700
     * written to such a column. To avoid clashes when the sequence returns an existing ID this helper will
8701
     * update the sequence to the current max value of the column.
8702
     *
8703
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8704
     * @param string $tableName
8705
     */
8706
    protected function postProcessPostgresqlInsert(Connection $connection, string $tableName)
8707
    {
8708
        $queryBuilder = $connection->createQueryBuilder();
8709
        $queryBuilder->getRestrictions()->removeAll();
8710
        $row = $queryBuilder->select('PGT.schemaname', 'S.relname', 'C.attname', 'T.relname AS tablename')
8711
            ->from('pg_class', 'S')
8712
            ->from('pg_depend', 'D')
8713
            ->from('pg_class', 'T')
8714
            ->from('pg_attribute', 'C')
8715
            ->from('pg_tables', 'PGT')
8716
            ->where(
8717
                $queryBuilder->expr()->eq('S.relkind', $queryBuilder->quote('S')),
8718
                $queryBuilder->expr()->eq('S.oid', $queryBuilder->quoteIdentifier('D.objid')),
8719
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('T.oid')),
8720
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('C.attrelid')),
8721
                $queryBuilder->expr()->eq('D.refobjsubid', $queryBuilder->quoteIdentifier('C.attnum')),
8722
                $queryBuilder->expr()->eq('T.relname', $queryBuilder->quoteIdentifier('PGT.tablename')),
8723
                $queryBuilder->expr()->eq('PGT.tablename', $queryBuilder->quote($tableName))
8724
            )
8725
            ->setMaxResults(1)
8726
            ->execute()
8727
            ->fetch();
8728
8729
        if ($row !== false) {
8730
            $connection->exec(
8731
                sprintf(
8732
                    'SELECT SETVAL(%s, COALESCE(MAX(%s), 0)+1, FALSE) FROM %s',
8733
                    $connection->quote($row['schemaname'] . '.' . $row['relname']),
8734
                    $connection->quoteIdentifier($row['attname']),
8735
                    $connection->quoteIdentifier($row['schemaname'] . '.' . $row['tablename'])
8736
                )
8737
            );
8738
        }
8739
    }
8740
8741
    /**
8742
     * Return the cache entry identifier for field evals
8743
     *
8744
     * @param string $additionalIdentifier
8745
     * @return string
8746
     */
8747
    protected function getFieldEvalCacheIdentifier($additionalIdentifier)
8748
    {
8749
        return 'core-datahandler-eval-' . md5($additionalIdentifier);
8750
    }
8751
8752
    /**
8753
     * @return RelationHandler
8754
     */
8755
    protected function createRelationHandlerInstance()
8756
    {
8757
        $isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces');
8758
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
8759
        $relationHandler->setWorkspaceId($this->BE_USER->workspace);
8760
        $relationHandler->setUseLiveReferenceIds($isWorkspacesLoaded);
8761
        $relationHandler->setUseLiveParentIds($isWorkspacesLoaded);
8762
        return $relationHandler;
8763
    }
8764
8765
    /**
8766
     * Create and returns an instance of the CacheManager
8767
     *
8768
     * @return CacheManager
8769
     */
8770
    protected function getCacheManager()
8771
    {
8772
        return GeneralUtility::makeInstance(CacheManager::class);
8773
    }
8774
8775
    /**
8776
     * Gets the resourceFactory
8777
     *
8778
     * @return ResourceFactory
8779
     */
8780
    protected function getResourceFactory()
8781
    {
8782
        return GeneralUtility::makeInstance(ResourceFactory::class);
8783
    }
8784
8785
    /**
8786
     * @return LanguageService
8787
     */
8788
    protected function getLanguageService()
8789
    {
8790
        return $GLOBALS['LANG'];
8791
    }
8792
8793
    public function getHistoryRecords(): array
8794
    {
8795
        return $this->historyRecords;
8796
    }
8797
}
8798