Completed
Push — master ( ded3c9...01c6cd )
by
unknown
28:49 queued 13:48
created

DataHandler::checkValueForSlug()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 17
nc 10
nop 7
dl 0
loc 28
rs 9.3888
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
     * User by function checkRecordInsertAccess() to store whether a record can be inserted on a page id
453
     *
454
     * @var array
455
     */
456
    protected $recInsertAccessCache = [];
457
458
    /**
459
     * Caching array for check of whether records are in a webmount
460
     *
461
     * @var array
462
     */
463
    protected $isRecordInWebMount_Cache = [];
464
465
    /**
466
     * Caching array for page ids in webmounts
467
     *
468
     * @var array
469
     */
470
    protected $isInWebMount_Cache = [];
471
472
    /**
473
     * Used for caching page records in pageInfo()
474
     *
475
     * @var array
476
     */
477
    protected $pageCache = [];
478
479
    // Other arrays:
480
    /**
481
     * For accumulation of MM relations that must be written after new records are created.
482
     *
483
     * @var array
484
     */
485
    public $dbAnalysisStore = [];
486
487
    /**
488
     * Used for tracking references that might need correction after operations
489
     *
490
     * @var array
491
     */
492
    public $registerDBList = [];
493
494
    /**
495
     * Used for tracking references that might need correction in pid field after operations (e.g. IRRE)
496
     *
497
     * @var array
498
     */
499
    public $registerDBPids = [];
500
501
    /**
502
     * Used by the copy action to track the ids of new pages so subpages are correctly inserted!
503
     * THIS is internally cleared for each executed copy operation! DO NOT USE THIS FROM OUTSIDE!
504
     * Read from copyMappingArray_merged instead which is accumulating this information.
505
     *
506
     * NOTE: This is used by some outside scripts (e.g. hooks), as the results in $copyMappingArray_merged
507
     * are only available after an action has been completed.
508
     *
509
     * @var array
510
     */
511
    public $copyMappingArray = [];
512
513
    /**
514
     * Array used for remapping uids and values at the end of process_datamap
515
     *
516
     * @var array
517
     */
518
    public $remapStack = [];
519
520
    /**
521
     * Array used for remapping uids and values at the end of process_datamap
522
     * (e.g. $remapStackRecords[<table>][<uid>] = <index in $remapStack>)
523
     *
524
     * @var array
525
     */
526
    public $remapStackRecords = [];
527
528
    /**
529
     * Array used for checking whether new children need to be remapped
530
     *
531
     * @var array
532
     */
533
    protected $remapStackChildIds = [];
534
535
    /**
536
     * Array used for executing addition actions after remapping happened (set processRemapStack())
537
     *
538
     * @var array
539
     */
540
    protected $remapStackActions = [];
541
542
    /**
543
     * Array used for executing post-processing on the reference index
544
     *
545
     * @var array
546
     */
547
    protected $remapStackRefIndex = [];
548
549
    /**
550
     * Array used for additional calls to $this->updateRefIndex
551
     *
552
     * @var array
553
     */
554
    public $updateRefIndexStack = [];
555
556
    /**
557
     * Tells, that this DataHandler instance was called from \TYPO3\CMS\Impext\ImportExport.
558
     * This variable is set by \TYPO3\CMS\Impext\ImportExport
559
     *
560
     * @var bool
561
     */
562
    public $callFromImpExp = false;
563
564
    // Various
565
566
    /**
567
     * Set to "currentRecord" during checking of values.
568
     *
569
     * @var array
570
     */
571
    public $checkValue_currentRecord = [];
572
573
    /**
574
     * Disable delete clause
575
     *
576
     * @var bool
577
     */
578
    protected $disableDeleteClause = false;
579
580
    /**
581
     * @var array
582
     */
583
    protected $checkModifyAccessListHookObjects;
584
585
    /**
586
     * @var array
587
     */
588
    protected $version_remapMMForVersionSwap_reg;
589
590
    /**
591
     * The outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler:
592
     * This object instantiates itself on versioning and localization ...
593
     *
594
     * @var \TYPO3\CMS\Core\DataHandling\DataHandler
595
     */
596
    protected $outerMostInstance;
597
598
    /**
599
     * Internal cache for collecting records that should trigger cache clearing
600
     *
601
     * @var array
602
     */
603
    protected static $recordsToClearCacheFor = [];
604
605
    /**
606
     * Internal cache for pids of records which were deleted. It's not possible
607
     * to retrieve the parent folder/page at a later stage
608
     *
609
     * @var array
610
     */
611
    protected static $recordPidsForDeletedRecords = [];
612
613
    /**
614
     * Runtime Cache to store and retrieve data computed for a single request
615
     *
616
     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
617
     */
618
    protected $runtimeCache;
619
620
    /**
621
     * Prefix for the cache entries of nested element calls since the runtimeCache has a global scope.
622
     *
623
     * @var string
624
     */
625
    protected $cachePrefixNestedElementCalls = 'core-datahandler-nestedElementCalls-';
626
627
    /**
628
     * Sets up the data handler cache and some additional options, the main logic is done in the start() method.
629
     */
630
    public function __construct()
631
    {
632
        $this->checkStoredRecords = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecords'];
633
        $this->checkStoredRecords_loose = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecordsLoose'];
634
        $this->runtimeCache = $this->getRuntimeCache();
635
        $this->pagePermissionAssembler = GeneralUtility::makeInstance(PagePermissionAssembler::class, $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions']);
636
    }
637
638
    /**
639
     * @param array $control
640
     */
641
    public function setControl(array $control)
642
    {
643
        $this->control = $control;
644
    }
645
646
    /**
647
     * Initializing.
648
     * For details, see 'TYPO3 Core API' document.
649
     * This function does not start the processing of data, but merely initializes the object
650
     *
651
     * @param array $data Data to be modified or inserted in the database
652
     * @param array $cmd Commands to copy, move, delete, localize, versionize records.
653
     * @param BackendUserAuthentication|null $altUserObject An alternative userobject you can set instead of the default, which is $GLOBALS['BE_USER']
654
     */
655
    public function start($data, $cmd, $altUserObject = null)
656
    {
657
        // Initializing BE_USER
658
        $this->BE_USER = is_object($altUserObject) ? $altUserObject : $GLOBALS['BE_USER'];
659
        $this->userid = $this->BE_USER->user['uid'] ?? 0;
660
        $this->username = $this->BE_USER->user['username'] ?? '';
661
        $this->admin = $this->BE_USER->user['admin'] ?? false;
662
        if ($this->BE_USER->uc['recursiveDelete'] ?? false) {
663
            $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...
664
        }
665
666
        // set correlation id for each new set of data or commands
667
        $this->correlationId = CorrelationId::forScope(
668
            md5(StringUtility::getUniqueId(self::class))
669
        );
670
671
        // Get default values from user TSconfig
672
        $tcaDefaultOverride = $this->BE_USER->getTSConfig()['TCAdefaults.'] ?? null;
673
        if (is_array($tcaDefaultOverride)) {
674
            $this->setDefaultsFromUserTS($tcaDefaultOverride);
675
        }
676
677
        // Initializing default permissions for pages
678
        $defaultPermissions = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'];
679
        if (isset($defaultPermissions['user'])) {
680
            $this->defaultPermissions['user'] = $defaultPermissions['user'];
681
        }
682
        if (isset($defaultPermissions['group'])) {
683
            $this->defaultPermissions['group'] = $defaultPermissions['group'];
684
        }
685
        if (isset($defaultPermissions['everybody'])) {
686
            $this->defaultPermissions['everybody'] = $defaultPermissions['everybody'];
687
        }
688
        // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
689
        if (!$this->admin) {
690
            $this->excludedTablesAndFields = array_flip($this->getExcludeListArray());
691
        }
692
        // Setting the data and cmd arrays
693
        if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
694
            reset($data);
695
            $this->datamap = $data;
696
        }
697
        if (is_array($cmd)) {
0 ignored issues
show
introduced by
The condition is_array($cmd) is always true.
Loading history...
698
            reset($cmd);
699
            $this->cmdmap = $cmd;
700
        }
701
    }
702
703
    /**
704
     * Function that can mirror input values in datamap-array to other uid numbers.
705
     * 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]
706
     *
707
     * @param array $mirror This array has the syntax $mirror[table_name][uid] = [list of uids to copy data-value TO!]
708
     */
709
    public function setMirror($mirror)
710
    {
711
        if (!is_array($mirror)) {
0 ignored issues
show
introduced by
The condition is_array($mirror) is always true.
Loading history...
712
            return;
713
        }
714
715
        foreach ($mirror as $table => $uid_array) {
716
            if (!isset($this->datamap[$table])) {
717
                continue;
718
            }
719
720
            foreach ($uid_array as $id => $uidList) {
721
                if (!isset($this->datamap[$table][$id])) {
722
                    continue;
723
                }
724
725
                $theIdsInArray = GeneralUtility::trimExplode(',', $uidList, true);
726
                foreach ($theIdsInArray as $copyToUid) {
727
                    $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
728
                }
729
            }
730
        }
731
    }
732
733
    /**
734
     * Initializes default values coming from User TSconfig
735
     *
736
     * @param array $userTS User TSconfig array
737
     */
738
    public function setDefaultsFromUserTS($userTS)
739
    {
740
        if (!is_array($userTS)) {
0 ignored issues
show
introduced by
The condition is_array($userTS) is always true.
Loading history...
741
            return;
742
        }
743
744
        foreach ($userTS as $k => $v) {
745
            $k = mb_substr($k, 0, -1);
746
            if (!$k || !is_array($v) || !isset($GLOBALS['TCA'][$k])) {
747
                continue;
748
            }
749
750
            if (is_array($this->defaultValues[$k])) {
751
                $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
752
            } else {
753
                $this->defaultValues[$k] = $v;
754
            }
755
        }
756
    }
757
758
    /**
759
     * Dummy method formerly used for file handling.
760
     *
761
     * @deprecated since TYPO3 v10.0, will be removed in TYPO3 v11.0.
762
     */
763
    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...
764
    {
765
        trigger_error('DataHandler->process_uploads() will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
766
    }
767
768
    /*********************************************
769
     *
770
     * HOOKS
771
     *
772
     *********************************************/
773
    /**
774
     * Hook: processDatamap_afterDatabaseOperations
775
     * (calls $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);)
776
     *
777
     * Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
778
     * but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
779
     *
780
     * @param array $hookObjectsArr (reference) Array with hook objects
781
     * @param string $status (reference) Status of the current operation, 'new' or 'update
782
     * @param string $table (reference) The table currently processing data for
783
     * @param string $id (reference) The record uid currently processing data for, [integer] or [string] (like 'NEW...')
784
     * @param array $fieldArray (reference) The field array of a record
785
     */
786
    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...
787
    {
788
        // Process hook directly:
789
        if (!isset($this->remapStackRecords[$table][$id])) {
790
            foreach ($hookObjectsArr as $hookObj) {
791
                if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
792
                    $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
793
                }
794
            }
795
        } else {
796
            $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = [
797
                'status' => $status,
798
                'fieldArray' => $fieldArray,
799
                'hookObjectsArr' => $hookObjectsArr
800
            ];
801
        }
802
    }
803
804
    /**
805
     * Gets the 'checkModifyAccessList' hook objects.
806
     * The first call initializes the accordant objects.
807
     *
808
     * @return array The 'checkModifyAccessList' hook objects (if any)
809
     * @throws \UnexpectedValueException
810
     */
811
    protected function getCheckModifyAccessListHookObjects()
812
    {
813
        if (!isset($this->checkModifyAccessListHookObjects)) {
814
            $this->checkModifyAccessListHookObjects = [];
815
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] ?? [] as $className) {
816
                $hookObject = GeneralUtility::makeInstance($className);
817
                if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
818
                    throw new \UnexpectedValueException($className . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
819
                }
820
                $this->checkModifyAccessListHookObjects[] = $hookObject;
821
            }
822
        }
823
        return $this->checkModifyAccessListHookObjects;
824
    }
825
826
    /*********************************************
827
     *
828
     * PROCESSING DATA
829
     *
830
     *********************************************/
831
    /**
832
     * Processing the data-array
833
     * Call this function to process the data-array set by start()
834
     *
835
     * @return bool|void
836
     */
837
    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...
838
    {
839
        $this->controlActiveElements();
840
841
        // Keep versionized(!) relations here locally:
842
        $registerDBList = [];
843
        $this->registerElementsToBeDeleted();
844
        $this->datamap = $this->unsetElementsToBeDeleted($this->datamap);
845
        // Editing frozen:
846
        if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
847
            $this->newlog('All editing in this workspace has been frozen!', SystemLogErrorClassification::USER_ERROR);
848
            return false;
849
        }
850
        // First prepare user defined objects (if any) for hooks which extend this function:
851
        $hookObjectsArr = [];
852
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] ?? [] as $className) {
853
            $hookObject = GeneralUtility::makeInstance($className);
854
            if (method_exists($hookObject, 'processDatamap_beforeStart')) {
855
                $hookObject->processDatamap_beforeStart($this);
856
            }
857
            $hookObjectsArr[] = $hookObject;
858
        }
859
        // Pre-process data-map and synchronize localization states
860
        $this->datamap = GeneralUtility::makeInstance(SlugEnricher::class)->enrichDataMap($this->datamap);
861
        $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER)->process();
862
        // 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.
863
        $orderOfTables = [];
864
        // Set pages first.
865
        if (isset($this->datamap['pages'])) {
866
            $orderOfTables[] = 'pages';
867
        }
868
        $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
869
        // Process the tables...
870
        foreach ($orderOfTables as $table) {
871
            // Check if
872
            //	   - table is set in $GLOBALS['TCA'],
873
            //	   - table is NOT readOnly
874
            //	   - the table is set with content in the data-array (if not, there's nothing to process...)
875
            //	   - permissions for tableaccess OK
876
            $modifyAccessList = $this->checkModifyAccessList($table);
877
            if (!$modifyAccessList) {
878
                $this->log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
879
            }
880
            if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
881
                continue;
882
            }
883
884
            if ($this->reverseOrder) {
885
                $this->datamap[$table] = array_reverse($this->datamap[$table], 1);
886
            }
887
            // For each record from the table, do:
888
            // $id is the record uid, may be a string if new records...
889
            // $incomingFieldArray is the array of fields
890
            foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
891
                if (!is_array($incomingFieldArray)) {
892
                    continue;
893
                }
894
                $theRealPid = null;
895
896
                // Hook: processDatamap_preProcessFieldArray
897
                foreach ($hookObjectsArr as $hookObj) {
898
                    if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
899
                        $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
900
                    }
901
                }
902
                // ******************************
903
                // Checking access to the record
904
                // ******************************
905
                $createNewVersion = false;
906
                $recordAccess = false;
907
                $old_pid_value = '';
908
                // Is it a new record? (Then Id is a string)
909
                if (!MathUtility::canBeInterpretedAsInteger($id)) {
910
                    // Get a fieldArray with tca default values
911
                    $fieldArray = $this->newFieldArray($table);
912
                    // A pid must be set for new records.
913
                    if (isset($incomingFieldArray['pid'])) {
914
                        $pid_value = $incomingFieldArray['pid'];
915
                        // Checking and finding numerical pid, it may be a string-reference to another value
916
                        $canProceed = true;
917
                        // If a NEW... id
918
                        if (strpos($pid_value, 'NEW') !== false) {
919
                            if ($pid_value[0] === '-') {
920
                                $negFlag = -1;
921
                                $pid_value = substr($pid_value, 1);
922
                            } else {
923
                                $negFlag = 1;
924
                            }
925
                            // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
926
                            if (isset($this->substNEWwithIDs[$pid_value])) {
927
                                if ($negFlag === 1) {
928
                                    $old_pid_value = $this->substNEWwithIDs[$pid_value];
929
                                }
930
                                $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
931
                            } else {
932
                                $canProceed = false;
933
                            }
934
                        }
935
                        $pid_value = (int)$pid_value;
936
                        if ($canProceed) {
937
                            $fieldArray = $this->resolveSortingAndPidForNewRecord($table, $pid_value, $fieldArray);
938
                        }
939
                    }
940
                    $theRealPid = $fieldArray['pid'];
941
                    // Checks if records can be inserted on this $pid.
942
                    // If this is a page translation, the check needs to be done for the l10n_parent record
943
                    if ($table === 'pages' && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0 && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0) {
944
                        $recordAccess = $this->checkRecordInsertAccess($table, $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]);
945
                    } else {
946
                        $recordAccess = $this->checkRecordInsertAccess($table, $theRealPid);
947
                    }
948
                    if ($recordAccess) {
949
                        $this->addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray);
950
                        $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
951
                        if (!$recordAccess) {
952
                            $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
953
                        } elseif (!$this->bypassWorkspaceRestrictions && !$this->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
954
                            // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
955
                            // So, if no live records were allowed in the current workspace, we have to create a new version of this record
956
                            if (BackendUtility::isTableWorkspaceEnabled($table)) {
957
                                $createNewVersion = true;
958
                            } else {
959
                                $recordAccess = false;
960
                                $this->newlog('Record could not be created in this workspace', SystemLogErrorClassification::USER_ERROR);
961
                            }
962
                        }
963
                    }
964
                    // Yes new record, change $record_status to 'insert'
965
                    $status = 'new';
966
                } else {
967
                    // Nope... $id is a number
968
                    $fieldArray = [];
969
                    $recordAccess = $this->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $hookObjectsArr);
970
                    if (!$recordAccess) {
971
                        if ($this->enableLogging) {
972
                            $propArr = $this->getRecordProperties($table, $id);
973
                            $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']);
974
                        }
975
                        continue;
976
                    }
977
                    // Next check of the record permissions (internals)
978
                    $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
979
                    if (!$recordAccess) {
980
                        $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
981
                    } else {
982
                        // Here we fetch the PID of the record that we point to...
983
                        $tempdata = $this->recordInfo($table, $id, 'pid' . (BackendUtility::isTableWorkspaceEnabled($table) ? ',t3ver_oid,t3ver_wsid,t3ver_stage' : ''));
984
                        $theRealPid = $tempdata['pid'] ?? null;
985
                        // Use the new id of the versionized record we're trying to write to:
986
                        // (This record is a child record of a parent and has already been versionized.)
987
                        if (!empty($this->autoVersionIdMap[$table][$id])) {
988
                            // For the reason that creating a new version of this record, automatically
989
                            // created related child records (e.g. "IRRE"), update the accordant field:
990
                            $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
991
                            // Use the new id of the copied/versionized record:
992
                            $id = $this->autoVersionIdMap[$table][$id];
993
                            $recordAccess = true;
994
                        } elseif (!$this->bypassWorkspaceRestrictions && ($errorCode = $this->BE_USER->workspaceCannotEditRecord($table, $tempdata))) {
995
                            $recordAccess = false;
996
                            // Versioning is required and it must be offline version!
997
                            // Check if there already is a workspace version
998
                            $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
999
                            if ($workspaceVersion) {
1000
                                $id = $workspaceVersion['uid'];
1001
                                $recordAccess = true;
1002
                            } elseif ($this->BE_USER->workspaceAllowAutoCreation($table, $id, $theRealPid)) {
1003
                                // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1004
                                $this->pagetreeNeedsRefresh = true;
1005
1006
                                /** @var DataHandler $tce */
1007
                                $tce = GeneralUtility::makeInstance(__CLASS__);
1008
                                $tce->enableLogging = $this->enableLogging;
1009
                                // Setting up command for creating a new version of the record:
1010
                                $cmd = [];
1011
                                $cmd[$table][$id]['version'] = [
1012
                                    'action' => 'new',
1013
                                    // Default is to create a version of the individual records
1014
                                    'label' => 'Auto-created for WS #' . $this->BE_USER->workspace
1015
                                ];
1016
                                $tce->start([], $cmd, $this->BE_USER);
1017
                                $tce->process_cmdmap();
1018
                                $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
1019
                                // If copying was successful, share the new uids (also of related children):
1020
                                if (!empty($tce->copyMappingArray[$table][$id])) {
1021
                                    foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
1022
                                        foreach ($origIdArray as $origId => $newId) {
1023
                                            $this->autoVersionIdMap[$origTable][$origId] = $newId;
1024
                                        }
1025
                                    }
1026
                                    // Update registerDBList, that holds the copied relations to child records:
1027
                                    $registerDBList = array_merge($registerDBList, $tce->registerDBList);
1028
                                    // For the reason that creating a new version of this record, automatically
1029
                                    // created related child records (e.g. "IRRE"), update the accordant field:
1030
                                    $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1031
                                    // Use the new id of the copied/versionized record:
1032
                                    $id = $this->autoVersionIdMap[$table][$id];
1033
                                    $recordAccess = true;
1034
                                } else {
1035
                                    $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);
1036
                                }
1037
                            } else {
1038
                                $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);
1039
                            }
1040
                        }
1041
                    }
1042
                    // The default is 'update'
1043
                    $status = 'update';
1044
                }
1045
                // If access was granted above, proceed to create or update record:
1046
                if (!$recordAccess) {
1047
                    continue;
1048
                }
1049
1050
                // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
1051
                [$tscPID] = BackendUtility::getTSCpid($table, $id, $old_pid_value ?: $fieldArray['pid']);
1052
                if ($status === 'new' && $table === 'pages') {
1053
                    $fieldArray = $this->pagePermissionAssembler->applyDefaults(
1054
                        $fieldArray,
1055
                        (int)$tscPID,
1056
                        (int)$this->userid,
1057
                        (int)$this->BE_USER->firstMainGroup
1058
                    );
1059
                }
1060
                // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1061
                $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1062
                $newVersion_placeholderFieldArray = [];
1063
                if ($createNewVersion) {
1064
                    // create a placeholder array with already processed field content
1065
                    $newVersion_placeholderFieldArray = $fieldArray;
1066
                }
1067
                // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations to field!
1068
                // Forcing some values unto field array:
1069
                // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1070
                $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1071
                if ($createNewVersion) {
1072
                    $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1073
                }
1074
                // Setting system fields
1075
                if ($status === 'new') {
1076
                    if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1077
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1078
                        if ($createNewVersion) {
1079
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1080
                        }
1081
                    }
1082
                    if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1083
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1084
                        if ($createNewVersion) {
1085
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1086
                        }
1087
                    }
1088
                } elseif ($this->checkSimilar) {
1089
                    // Removing fields which are equal to the current value:
1090
                    $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1091
                }
1092
                if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1093
                    $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1094
                    if ($createNewVersion) {
1095
                        $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1096
                    }
1097
                }
1098
                // Set stage to "Editing" to make sure we restart the workflow
1099
                if (BackendUtility::isTableWorkspaceEnabled($table)) {
1100
                    $fieldArray['t3ver_stage'] = 0;
1101
                }
1102
                // Hook: processDatamap_postProcessFieldArray
1103
                foreach ($hookObjectsArr as $hookObj) {
1104
                    if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1105
                        $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1106
                    }
1107
                }
1108
                // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1109
                // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already
1110
                if (is_array($fieldArray)) {
1111
                    if ($status === 'new') {
1112
                        if ($table === 'pages') {
1113
                            // for new pages always a refresh is needed
1114
                            $this->pagetreeNeedsRefresh = true;
1115
                        }
1116
1117
                        // This creates a new version of the record with online placeholder and offline version
1118
                        if ($createNewVersion) {
1119
                            // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1120
                            $this->pagetreeNeedsRefresh = true;
1121
1122
                            // Setting placeholder state value for temporary record
1123
                            $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1124
                            // Setting workspace - only so display of placeholders can filter out those from other workspaces.
1125
                            $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1126
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $this->getPlaceholderTitleForTableLabel($table);
1127
                            // Saving placeholder as 'original'
1128
                            $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1129
                            // For the actual new offline version, set versioning values to point to placeholder
1130
                            $fieldArray['pid'] = $theRealPid;
1131
                            $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1132
                            // Setting placeholder state value for version (so it can know it is currently a new version...)
1133
                            $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1134
                            $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1135
                            // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1136
                            $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1137
                            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...
1138
                                // Processes fields of the placeholder record:
1139
                                $this->triggerRemapAction($table, $id, [$this, 'placeholderShadowing'], [$table, $phShadowId]);
1140
                                // Hold auto-versionized ids of placeholders:
1141
                                $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $phShadowId;
1142
                            }
1143
                        } else {
1144
                            $this->insertDB($table, $id, $fieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1145
                        }
1146
                    } else {
1147
                        if ($table === 'pages') {
1148
                            // only a certain number of fields needs to be checked for updates
1149
                            // if $this->checkSimilar is TRUE, fields with unchanged values are already removed here
1150
                            $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1151
                            if (!empty($fieldsToCheck)) {
1152
                                $this->pagetreeNeedsRefresh = true;
1153
                            }
1154
                        }
1155
                        $this->updateDB($table, $id, $fieldArray);
1156
                        $this->placeholderShadowing($table, $id);
1157
                    }
1158
                }
1159
                // Hook: processDatamap_afterDatabaseOperations
1160
                // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1161
                // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1162
                $this->hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1163
            }
1164
        }
1165
        // Process the stack of relations to remap/correct
1166
        $this->processRemapStack();
1167
        $this->dbAnalysisStoreExec();
1168
        // Hook: processDatamap_afterAllOperations
1169
        // Note: When this hook gets called, all operations on the submitted data have been finished.
1170
        foreach ($hookObjectsArr as $hookObj) {
1171
            if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1172
                $hookObj->processDatamap_afterAllOperations($this);
1173
            }
1174
        }
1175
        if ($this->isOuterMostInstance()) {
1176
            $this->processClearCacheQueue();
1177
            $this->resetElementsToBeDeleted();
1178
        }
1179
    }
1180
1181
    /**
1182
     * @param string $table
1183
     * @param string $value
1184
     * @param string $dbType
1185
     * @return string
1186
     */
1187
    protected function normalizeTimeFormat(string $table, string $value, string $dbType): string
1188
    {
1189
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1190
        $platform = $connection->getDatabasePlatform();
1191
        if ($platform instanceof SQLServerPlatform) {
1192
            $defaultLength = QueryHelper::getDateTimeFormats()[$dbType]['empty'];
1193
            $value = substr(
1194
                $value,
1195
                0,
1196
                strlen($defaultLength)
1197
            );
1198
        }
1199
        return $value;
1200
    }
1201
1202
    /**
1203
     * Sets the "sorting" DB field and the "pid" field of an incoming record that should be added (NEW1234)
1204
     * depending on the record that should be added or where it should be added.
1205
     *
1206
     * This method is called from process_datamap()
1207
     *
1208
     * @param string $table the table name of the record to insert
1209
     * @param int $pid the real PID (numeric) where the record should be
1210
     * @param array $fieldArray field+value pairs to add
1211
     * @return array the modified field array
1212
     */
1213
    protected function resolveSortingAndPidForNewRecord(string $table, int $pid, array $fieldArray): array
1214
    {
1215
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
1216
        // Points to a page on which to insert the element, possibly in the top of the page
1217
        if ($pid >= 0) {
1218
            // Ensure that the "pid" is not a translated page ID, but the default page ID
1219
            $pid = $this->getDefaultLanguagePageId($pid);
1220
            // The numerical pid is inserted in the data array
1221
            $fieldArray['pid'] = $pid;
1222
            // If this table is sorted we better find the top sorting number
1223
            if ($sortColumn) {
1224
                $fieldArray[$sortColumn] = $this->getSortNumber($table, 0, $pid);
1225
            }
1226
        } elseif ($sortColumn) {
1227
            // Points to another record before itself
1228
            // If this table is sorted we better find the top sorting number
1229
            // Because $pid is < 0, getSortNumber() returns an array
1230
            $sortingInfo = $this->getSortNumber($table, 0, $pid);
1231
            $fieldArray['pid'] = $sortingInfo['pid'];
1232
            $fieldArray[$sortColumn] = $sortingInfo['sortNumber'];
1233
        } else {
1234
            // Here we fetch the PID of the record that we point to
1235
            $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

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

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

1498
                    $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...
1499
                }
1500
                return $res;
1501
            }
1502
            if ($status === 'update') {
1503
                // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1504
                $onlyAllowedTables = $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1505
                if ($onlyAllowedTables) {
1506
                    // use the real page id (default language)
1507
                    $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

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

1508
                    $theWrongTables = $this->doesPageHaveUnallowedTables($recordId, /** @scrutinizer ignore-type */ $value);
Loading history...
1509
                    if ($theWrongTables) {
1510
                        if ($this->enableLogging) {
1511
                            $propArr = $this->getRecordProperties($table, $id);
1512
                            $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']);
1513
                        }
1514
                        return $res;
1515
                    }
1516
                }
1517
            }
1518
        }
1519
1520
        $curValue = null;
1521
        if ((int)$id !== 0) {
1522
            // Get current value:
1523
            $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

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

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

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

2766
                $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, /** @scrutinizer ignore-type */ $prep);
Loading history...
2767
                if ($oldRelations != $newRelations) {
2768
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
2769
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
2770
                } else {
2771
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
2772
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
2773
                }
2774
            } else {
2775
                $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
2776
            }
2777
            $valueArray = $dbAnalysis->countItems();
2778
        } else {
2779
            $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

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

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

3196
        BackendUtility::workspaceOL($table, /** @scrutinizer ignore-type */ $row, $this->BE_USER->workspace);
Loading history...
3197
        $row = BackendUtility::purgeComputedPropertiesFromRecord($row);
3198
3199
        // Initializing:
3200
        $theNewID = StringUtility::getUniqueId('NEW');
3201
        $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3202
        $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3203
        // Getting "copy-after" fields if applicable:
3204
        $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

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

3500
        $row = array_merge(/** @scrutinizer ignore-type */ $row, $overrideArray);
Loading history...
3501
        // Traverse ALL fields of the selected record:
3502
        foreach ($row as $field => $value) {
3503
            if (!in_array($field, $nonFields, true)) {
3504
                // Get TCA configuration for the field:
3505
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3506
                if (is_array($conf)) {
3507
                    // Processing based on the TCA config field type (files, references, flexforms...)
3508
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3509
                }
3510
                // Add value to array.
3511
                $row[$field] = $value;
3512
            }
3513
        }
3514
        $row['pid'] = $pid;
3515
        // Setting original UID:
3516
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3517
            $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3518
        }
3519
        // Do the copy by internal function
3520
        $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3521
        if ($theNewSQLID) {
3522
            $this->dbAnalysisStoreExec();
3523
            $this->dbAnalysisStore = [];
3524
            return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3525
        }
3526
        return null;
3527
    }
3528
3529
    /**
3530
     * Inserts a record in the database, passing TCA configuration values through checkValue() but otherwise does NOTHING and checks nothing regarding permissions.
3531
     * 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...
3532
     *
3533
     * @param string $table Table name
3534
     * @param array $fieldArray Field array to insert as a record
3535
     * @param int $realPid The value of PID field.
3536
     * @return int Returns the new ID of the record (if applicable)
3537
     */
3538
    public function insertNewCopyVersion($table, $fieldArray, $realPid)
3539
    {
3540
        $id = StringUtility::getUniqueId('NEW');
3541
        // $fieldArray is set as current record.
3542
        // 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...
3543
        $this->checkValue_currentRecord = $fieldArray;
3544
        // Makes sure that transformations aren't processed on the copy.
3545
        $backupDontProcessTransformations = $this->dontProcessTransformations;
3546
        $this->dontProcessTransformations = true;
3547
        // Traverse record and input-process each value:
3548
        foreach ($fieldArray as $field => $fieldValue) {
3549
            if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3550
                // Evaluating the value.
3551
                $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0, $fieldArray);
3552
                if (isset($res['value'])) {
3553
                    $fieldArray[$field] = $res['value'];
3554
                }
3555
            }
3556
        }
3557
        // System fields being set:
3558
        if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3559
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3560
        }
3561
        if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3562
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3563
        }
3564
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3565
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3566
        }
3567
        // Finally, insert record:
3568
        $this->insertDB($table, $id, $fieldArray, true);
3569
        // Resets dontProcessTransformations to the previous state.
3570
        $this->dontProcessTransformations = $backupDontProcessTransformations;
3571
        // Return new id:
3572
        return $this->substNEWwithIDs[$id];
3573
    }
3574
3575
    /**
3576
     * Processing/Preparing content for copyRecord() function
3577
     *
3578
     * @param string $table Table name
3579
     * @param int $uid Record uid
3580
     * @param string $field Field name being processed
3581
     * @param string $value Input value to be processed.
3582
     * @param array $row Record array
3583
     * @param array $conf TCA field configuration
3584
     * @param int $realDestPid Real page id (pid) the record is copied to
3585
     * @param int $language Language ID (from sys_language table) used in the duplicated record
3586
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3587
     * @return array|string
3588
     * @internal
3589
     * @see copyRecord()
3590
     */
3591
    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...
3592
    {
3593
        $inlineSubType = $this->getInlineFieldType($conf);
3594
        // Get the localization mode for the current (parent) record (keep|select):
3595
        // Register if there are references to take care of or MM is used on an inline field (no change to value):
3596
        if ($this->isReferenceField($conf) || $inlineSubType === 'mm') {
3597
            $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language);
3598
        } elseif ($inlineSubType !== false) {
3599
            $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions);
3600
        }
3601
        // 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())
3602
        if ($conf['type'] === 'flex') {
3603
            // Get current value array:
3604
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
3605
            $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
3606
                ['config' => $conf],
3607
                $table,
3608
                $field,
3609
                $row
3610
            );
3611
            $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
3612
            $currentValueArray = GeneralUtility::xml2array($value);
3613
            // Traversing the XML structure, processing files:
3614
            if (is_array($currentValueArray)) {
3615
                $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
3616
                // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3617
                $value = $currentValueArray;
3618
            }
3619
        }
3620
        return $value;
3621
    }
3622
3623
    /**
3624
     * Processes the children of an MM relation field (select, group, inline) when the parent record is copied.
3625
     *
3626
     * @param string $table
3627
     * @param int $uid
3628
     * @param string $field
3629
     * @param mixed $value
3630
     * @param array $conf
3631
     * @param string $language
3632
     * @return mixed
3633
     */
3634
    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...
3635
    {
3636
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
3637
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
3638
        $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3639
        $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3640
        // Localize referenced records of select fields:
3641
        $localizingNonManyToManyFieldReferences = empty($mmTable) && $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3642
        /** @var RelationHandler $dbAnalysis */
3643
        $dbAnalysis = $this->createRelationHandlerInstance();
3644
        $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3645
        $purgeItems = false;
3646
        if ($language > 0 && $localizingNonManyToManyFieldReferences) {
3647
            foreach ($dbAnalysis->itemArray as $index => $item) {
3648
                // Since select fields can reference many records, check whether there's already a localization:
3649
                $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

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

3653
                    $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3654
                }
3655
            }
3656
            $purgeItems = true;
3657
        }
3658
3659
        if ($purgeItems || $mmTable) {
3660
            $dbAnalysis->purgeItemArray();
3661
            $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

3661
            $value = implode(',', $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName));
Loading history...
3662
        }
3663
        // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3664
        if ($value) {
3665
            $this->registerDBList[$table][$uid][$field] = $value;
3666
        }
3667
3668
        return $value;
3669
    }
3670
3671
    /**
3672
     * Processes child records in an inline (IRRE) element when the parent record is copied.
3673
     *
3674
     * @param string $table
3675
     * @param int $uid
3676
     * @param string $field
3677
     * @param mixed $value
3678
     * @param array $row
3679
     * @param array $conf
3680
     * @param int $realDestPid
3681
     * @param string $language
3682
     * @param array $workspaceOptions
3683
     * @return string
3684
     */
3685
    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...
3686
        $table,
3687
        $uid,
3688
        $field,
3689
        $value,
3690
        $row,
3691
        $conf,
3692
        $realDestPid,
3693
        $language,
3694
        array $workspaceOptions
3695
    ) {
3696
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3697
        /** @var RelationHandler $dbAnalysis */
3698
        $dbAnalysis = $this->createRelationHandlerInstance();
3699
        $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3700
        // Walk through the items, copy them and remember the new id:
3701
        foreach ($dbAnalysis->itemArray as $k => $v) {
3702
            $newId = null;
3703
            // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3704
            if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3705
                // Children should be localized when the parent gets localized the first time, just do it:
3706
                $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

3706
                $newId = $this->localize($v['table'], $v['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3707
            } else {
3708
                if (!MathUtility::canBeInterpretedAsInteger($realDestPid)) {
3709
                    $newId = $this->copyRecord($v['table'], $v['id'], -$v['id']);
3710
                // If the destination page id is a NEW string, keep it on the same page
3711
                } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3712
                    // A filled $workspaceOptions indicated that this call
3713
                    // has it's origin in previous versionizeRecord() processing
3714
                    if (!empty($workspaceOptions)) {
3715
                        // Versions use live default id, thus the "new"
3716
                        // id is the original live default child record
3717
                        $newId = $v['id'];
3718
                        $this->versionizeRecord(
3719
                            $v['table'],
3720
                            $v['id'],
3721
                            $workspaceOptions['label'] ?? 'Auto-created for WS #' . $this->BE_USER->workspace,
3722
                            $workspaceOptions['delete'] ?? false
3723
                        );
3724
                    // Otherwise just use plain copyRecord() to create placeholders etc.
3725
                    } else {
3726
                        // If a record has been copied already during this request,
3727
                        // prevent superfluous duplication and use the existing copy
3728
                        if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3729
                            $newId = $this->copyMappingArray[$v['table']][$v['id']];
3730
                        } else {
3731
                            $newId = $this->copyRecord($v['table'], $v['id'], $realDestPid);
3732
                        }
3733
                    }
3734
                } else {
3735
                    // If a record has been copied already during this request,
3736
                    // prevent superfluous duplication and use the existing copy
3737
                    if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3738
                        $newId = $this->copyMappingArray[$v['table']][$v['id']];
3739
                    } else {
3740
                        $newId = $this->copyRecord_raw($v['table'], $v['id'], $realDestPid, [], $workspaceOptions);
3741
                    }
3742
                }
3743
            }
3744
            // If the current field is set on a page record, update the pid of related child records:
3745
            if ($table === 'pages') {
3746
                $this->registerDBPids[$v['table']][$v['id']] = $uid;
3747
            } elseif (isset($this->registerDBPids[$table][$uid])) {
3748
                $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][$uid];
3749
            }
3750
            $dbAnalysis->itemArray[$k]['id'] = $newId;
3751
        }
3752
        // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
3753
        $value = implode(',', $dbAnalysis->getValueArray());
3754
        $this->registerDBList[$table][$uid][$field] = $value;
3755
3756
        return $value;
3757
    }
3758
3759
    /**
3760
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
3761
     *
3762
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
3763
     * @param array $dsConf TCA field configuration (from Data Structure XML)
3764
     * @param string $dataValue The value of the flexForm field
3765
     * @param string $_1 Not used.
3766
     * @param string $_2 Not used.
3767
     * @param string $_3 Not used.
3768
     * @param array $workspaceOptions
3769
     * @return array Result array with key "value" containing the value of the processing.
3770
     * @see copyRecord()
3771
     * @see checkValue_flex_procInData_travDS()
3772
     */
3773
    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...
3774
    {
3775
        // Extract parameters:
3776
        [$table, $uid, $field, $realDestPid] = $pParams;
3777
        // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
3778
        if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
3779
            $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, [], $dsConf, $realDestPid, 0, $workspaceOptions);
3780
            $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
3781
        }
3782
        // Return
3783
        return ['value' => $dataValue];
3784
    }
3785
3786
    /**
3787
     * Find l10n-overlay records and perform the requested copy action for these records.
3788
     *
3789
     * @param string $table Record Table
3790
     * @param string $uid UID of the record in the default language
3791
     * @param string $destPid Position to copy to
3792
     * @param bool $first
3793
     * @param array $overrideValues
3794
     * @param string $excludeFields
3795
     */
3796
    public function copyL10nOverlayRecords($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '')
3797
    {
3798
        // There's no need to perform this for tables that are not localizable
3799
        if (!BackendUtility::isTableLocalizable($table)) {
3800
            return;
3801
        }
3802
3803
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3804
        $queryBuilder->getRestrictions()
3805
            ->removeAll()
3806
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3807
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
3808
3809
        $queryBuilder->select('*')
3810
            ->from($table)
3811
            ->where(
3812
                $queryBuilder->expr()->eq(
3813
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3814
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
3815
                )
3816
            );
3817
3818
        // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
3819
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
0 ignored issues
show
Bug introduced by
$destPid of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...:getTSconfig_pidValue(). ( Ignorable by Annotation )

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

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

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

3819
        $tscPID = BackendUtility::getTSconfig_pidValue($table, /** @scrutinizer ignore-type */ $uid, $destPid);
Loading history...
3820
        // Get the localized records to be copied
3821
        $l10nRecords = $queryBuilder->execute()->fetchAll();
3822
        if (is_array($l10nRecords)) {
3823
            $localizedDestPids = [];
3824
            // If $destPid < 0, then it is the uid of the original language record we are inserting after
3825
            if ($destPid < 0) {
3826
                // Get the localized records of the record we are inserting after
3827
                $queryBuilder->setParameter('pointer', abs($destPid), \PDO::PARAM_INT);
3828
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
3829
                // Index the localized record uids by language
3830
                if (is_array($destL10nRecords)) {
3831
                    foreach ($destL10nRecords as $record) {
3832
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
3833
                    }
3834
                }
3835
            }
3836
            $languageSourceMap = [
3837
                $uid => $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
3838
            ];
3839
            // Copy the localized records after the corresponding localizations of the destination record
3840
            foreach ($l10nRecords as $record) {
3841
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
3842
                if ($localizedDestPid < 0) {
3843
                    $newUid = $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
3844
                } else {
3845
                    $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

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

4028
                [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4029
                $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4030
                // Setting PID
4031
                $updateFields['pid'] = $destPid;
4032
                // Table is sorted by 'sortby'
4033
                if ($sortColumn && !isset($updateFields[$sortColumn])) {
4034
                    $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4035
                    $updateFields[$sortColumn] = $sortNumber;
4036
                }
4037
                // Check for child records that have also to be moved
4038
                $this->moveRecord_procFields($table, $uid, $destPid);
4039
                // Create query for update:
4040
                GeneralUtility::makeInstance(ConnectionPool::class)
4041
                    ->getConnectionForTable($table)
4042
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4043
                // Check for the localizations of that element
4044
                $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4045
                // Call post processing hooks:
4046
                foreach ($hookObjectsArr as $hookObj) {
4047
                    if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4048
                        $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4049
                    }
4050
                }
4051
4052
                $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4053
                if ($this->enableLogging) {
4054
                    // Logging...
4055
                    $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4056
                    if ($destPid != $propArr['pid']) {
4057
                        // Logged to old page
4058
                        $newPropArr = $this->getRecordProperties($table, $uid);
4059
                        $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4060
                        $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']);
4061
                        // Logged to new page
4062
                        $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);
4063
                    } else {
4064
                        // Logged to new page
4065
                        $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);
4066
                    }
4067
                }
4068
                // Clear cache after moving
4069
                $this->registerRecordIdForPageCacheClearing($table, $uid);
4070
                $this->fixUniqueInPid($table, $uid);
4071
                $this->fixUniqueInSite($table, (int)$uid);
4072
                if ($table === 'pages') {
4073
                    $this->fixUniqueInSiteForSubpages((int)$uid);
4074
                }
4075
            } elseif ($this->enableLogging) {
4076
                $destPropArr = $this->getRecordProperties('pages', $destPid);
4077
                $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']);
4078
            }
4079
        } elseif ($sortColumn) {
4080
            // Put after another record
4081
            // Table is being sorted
4082
            // Save the position to which the original record is requested to be moved
4083
            $originalRecordDestinationPid = $destPid;
4084
            $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4085
            // Setting the destPid to the new pid of the record.
4086
            $destPid = $sortInfo['pid'];
4087
            // If not an array, there was an error (which is already logged)
4088
            if (is_array($sortInfo)) {
4089
                if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4090
                    // clear cache before moving
4091
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4092
                    // We now update the pid and sortnumber (if not set for page translations)
4093
                    $updateFields['pid'] = $destPid;
4094
                    if (!isset($updateFields[$sortColumn])) {
4095
                        $updateFields[$sortColumn] = $sortInfo['sortNumber'];
4096
                    }
4097
                    // Check for child records that have also to be moved
4098
                    $this->moveRecord_procFields($table, $uid, $destPid);
4099
                    // Create query for update:
4100
                    GeneralUtility::makeInstance(ConnectionPool::class)
4101
                        ->getConnectionForTable($table)
4102
                        ->update($table, $updateFields, ['uid' => (int)$uid]);
4103
                    // Check for the localizations of that element
4104
                    $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4105
                    // Call post processing hooks:
4106
                    foreach ($hookObjectsArr as $hookObj) {
4107
                        if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4108
                            $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4109
                        }
4110
                    }
4111
                    $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4112
                    if ($this->enableLogging) {
4113
                        // Logging...
4114
                        $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4115
                        if ($destPid != $propArr['pid']) {
4116
                            // Logged to old page
4117
                            $newPropArr = $this->getRecordProperties($table, $uid);
4118
                            $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4119
                            $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']);
4120
                            // Logged to old page
4121
                            $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);
4122
                        } else {
4123
                            // Logged to old page
4124
                            $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);
4125
                        }
4126
                    }
4127
                    // Clear cache after moving
4128
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4129
                    $this->fixUniqueInPid($table, $uid);
4130
                    $this->fixUniqueInSite($table, (int)$uid);
4131
                    if ($table === 'pages') {
4132
                        $this->fixUniqueInSiteForSubpages((int)$uid);
4133
                    }
4134
                } elseif ($this->enableLogging) {
4135
                    $destPropArr = $this->getRecordProperties('pages', $destPid);
4136
                    $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']);
4137
                }
4138
            } else {
4139
                $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']);
4140
            }
4141
        }
4142
    }
4143
4144
    /**
4145
     * Walk through all fields of the moved record and look for children of e.g. the inline type.
4146
     * If child records are found, they are also move to the new $destPid.
4147
     *
4148
     * @param string $table Record Table
4149
     * @param int $uid Record UID
4150
     * @param int $destPid Position to move to
4151
     */
4152
    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...
4153
    {
4154
        $row = BackendUtility::getRecordWSOL($table, $uid);
4155
        if (is_array($row) && (int)$destPid !== (int)$row['pid']) {
4156
            $conf = $GLOBALS['TCA'][$table]['columns'];
4157
            foreach ($row as $field => $value) {
4158
                $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4159
            }
4160
        }
4161
    }
4162
4163
    /**
4164
     * Move child records depending on the field type of the parent record.
4165
     *
4166
     * @param string $table Record Table
4167
     * @param string $uid Record UID
4168
     * @param string $destPid Position to move to
4169
     * @param string $field Record field
4170
     * @param string $value Record field value
4171
     * @param array $conf TCA configuration of current field
4172
     */
4173
    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...
4174
    {
4175
        $dbAnalysis = null;
4176
        if ($conf['type'] === 'inline') {
4177
            $foreign_table = $conf['foreign_table'];
4178
            $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4179
            if ($foreign_table && $moveChildrenWithParent) {
4180
                $inlineType = $this->getInlineFieldType($conf);
4181
                if ($inlineType === 'list' || $inlineType === 'field') {
4182
                    if ($table === 'pages') {
4183
                        // If the inline elements are related to a page record,
4184
                        // make sure they reside at that page and not at its parent
4185
                        $destPid = $uid;
4186
                    }
4187
                    $dbAnalysis = $this->createRelationHandlerInstance();
4188
                    $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

4188
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
4189
                }
4190
            }
4191
        }
4192
        // Move the records
4193
        if (isset($dbAnalysis)) {
4194
            // Moving records to a positive destination will insert each
4195
            // record at the beginning, thus the order is reversed here:
4196
            foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4197
                $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

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

4259
                    $this->moveRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4260
                }
4261
            }
4262
        }
4263
    }
4264
4265
    /**
4266
     * Localizes a record to another system language
4267
     *
4268
     * @param string $table Table name
4269
     * @param int $uid Record uid (to be localized)
4270
     * @param int $language Language ID (from sys_language table)
4271
     * @return int|bool The uid (int) of the new translated record or FALSE (bool) if something went wrong
4272
     */
4273
    public function localize($table, $uid, $language)
4274
    {
4275
        $newId = false;
4276
        $uid = (int)$uid;
4277
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize-' . (string)$language) !== false) {
4278
            return false;
4279
        }
4280
4281
        $this->registerNestedElementCall($table, $uid, 'localize-' . (string)$language);
4282
        if (!$GLOBALS['TCA'][$table]['ctrl']['languageField'] || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
4283
            $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table ' . $table, SystemLogErrorClassification::USER_ERROR);
4284
            return false;
4285
        }
4286
        $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4287
        if (!$langRec) {
4288
            $this->newlog('Sys language UID "' . $language . '" not found valid!', SystemLogErrorClassification::USER_ERROR);
4289
            return false;
4290
        }
4291
4292
        if (!$this->doesRecordExist($table, $uid, Permission::PAGE_SHOW)) {
4293
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' without permission.', SystemLogErrorClassification::USER_ERROR);
4294
            return false;
4295
        }
4296
4297
        // Getting workspace overlay if possible - this will localize versions in workspace if any
4298
        $row = BackendUtility::getRecordWSOL($table, $uid);
4299
        if (!is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
4300
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' that did not exist!', SystemLogErrorClassification::USER_ERROR);
4301
            return false;
4302
        }
4303
4304
        // Make sure that records which are translated from another language than the default language have a correct
4305
        // localization source set themselves, before translating them to another language.
4306
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4307
            && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0) {
4308
            $localizationParentRecord = BackendUtility::getRecord(
4309
                $table,
4310
                $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4311
            );
4312
            if ((int)$localizationParentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4313
                $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);
4314
                return false;
4315
            }
4316
        }
4317
4318
        // Default language records must never have a localization parent as they are the origin of any translation.
4319
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4320
            && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4321
            $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);
4322
            return false;
4323
        }
4324
4325
        $recordLocalizations = BackendUtility::getRecordLocalization($table, $uid, $language, 'AND pid=' . (int)$row['pid']);
4326
4327
        if (!empty($recordLocalizations)) {
4328
            $this->newlog(sprintf(
4329
                'Localization failed: there already are localizations (%s) for language %d of the "%s" record %d!',
4330
                implode(', ', array_column($recordLocalizations, 'uid')),
4331
                $language,
4332
                $table,
4333
                $uid
4334
            ), 1);
4335
            return false;
4336
        }
4337
4338
        // Initialize:
4339
        $overrideValues = [];
4340
        // Set override values:
4341
        $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $langRec['uid'];
4342
        // If the translated record is a default language record, set it's uid as localization parent of the new record.
4343
        // If translating from any other language, no override is needed; we just can copy the localization parent of
4344
        // the original record (which is pointing to the correspondent default language record) to the new record.
4345
        // In copy / free mode the TransOrigPointer field is always set to 0, as no connection to the localization parent is wanted in that case.
4346
        // For pages, there is no "copy/free mode".
4347
        if (($this->useTransOrigPointerField || $table === 'pages') && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4348
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $uid;
4349
        } elseif (!$this->useTransOrigPointerField) {
4350
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = 0;
4351
        }
4352
        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
4353
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = $uid;
4354
        }
4355
        // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4356
        if (isset($GLOBALS['TCA'][$table]['ctrl']['type'])) {
4357
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4358
        }
4359
        // Set exclude Fields:
4360
        foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fCfg) {
4361
            $translateToMsg = '';
4362
            // Check if we are just prefixing:
4363
            if ($fCfg['l10n_mode'] === 'prefixLangTitle') {
4364
                if (($fCfg['config']['type'] === 'text' || $fCfg['config']['type'] === 'input') && (string)$row[$fN] !== '') {
4365
                    [$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

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

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

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

5038
        $row = BackendUtility::getRecord($table, /** @scrutinizer ignore-type */ $uid, '*', '', false);
Loading history...
5039
        if (empty($row)) {
5040
            return;
5041
        }
5042
        foreach ($row as $field => $value) {
5043
            $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5044
        }
5045
    }
5046
5047
    /**
5048
     * Process fields of a record to be deleted and search for special handling, like
5049
     * inline type, MM records, etc.
5050
     *
5051
     * @param string $table Record Table
5052
     * @param string $uid Record UID
5053
     * @param string $field Record field
5054
     * @param string $value Record field value
5055
     * @param array $conf TCA configuration of current field
5056
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5057
     * @see deleteRecord()
5058
     */
5059
    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...
5060
    {
5061
        if ($conf['type'] === 'inline') {
5062
            $foreign_table = $conf['foreign_table'];
5063
            if ($foreign_table) {
5064
                $inlineType = $this->getInlineFieldType($conf);
5065
                if ($inlineType === 'list' || $inlineType === 'field') {
5066
                    /** @var RelationHandler $dbAnalysis */
5067
                    $dbAnalysis = $this->createRelationHandlerInstance();
5068
                    $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
0 ignored issues
show
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $MMuid of TYPO3\CMS\Core\Database\RelationHandler::start(). ( Ignorable by Annotation )

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

5068
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
5069
                    $dbAnalysis->undeleteRecord = true;
5070
5071
                    $enableCascadingDelete = true;
5072
                    // non type save comparison is intended!
5073
                    if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5074
                        $enableCascadingDelete = false;
5075
                    }
5076
5077
                    // Walk through the items and remove them
5078
                    foreach ($dbAnalysis->itemArray as $v) {
5079
                        if (!$undeleteRecord) {
5080
                            if ($enableCascadingDelete) {
5081
                                $this->deleteAction($v['table'], $v['id']);
5082
                            }
5083
                        } else {
5084
                            $this->undeleteRecord($v['table'], $v['id']);
5085
                        }
5086
                    }
5087
                }
5088
            }
5089
        } elseif ($this->isReferenceField($conf)) {
5090
            $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5091
            $dbAnalysis = $this->createRelationHandlerInstance();
5092
            $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5093
            foreach ($dbAnalysis->itemArray as $v) {
5094
                $this->updateRefIndexStack[$table][$uid][] = [$v['table'], $v['id']];
5095
            }
5096
        }
5097
    }
5098
5099
    /**
5100
     * Find l10n-overlay records and perform the requested delete action for these records.
5101
     *
5102
     * @param string $table Record Table
5103
     * @param string $uid Record UID
5104
     */
5105
    public function deleteL10nOverlayRecords($table, $uid)
5106
    {
5107
        // Check whether table can be localized
5108
        if (!BackendUtility::isTableLocalizable($table)) {
5109
            return;
5110
        }
5111
5112
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5113
        $queryBuilder->getRestrictions()
5114
            ->removeAll()
5115
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5116
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
5117
5118
        $queryBuilder->select('*')
5119
            ->from($table)
5120
            ->where(
5121
                $queryBuilder->expr()->eq(
5122
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5123
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5124
                )
5125
            );
5126
5127
        $result = $queryBuilder->execute();
5128
        while ($record = $result->fetch()) {
5129
            // Ignore workspace delete placeholders. Those records have been marked for
5130
            // deletion before - deleting them again in a workspace would revert that state.
5131
            if ((int)$this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5132
                BackendUtility::workspaceOL($table, $record, $this->BE_USER->workspace);
5133
                if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5134
                    continue;
5135
                }
5136
            }
5137
            $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5138
        }
5139
    }
5140
5141
    /*********************************************
5142
     *
5143
     * Cmd: Versioning
5144
     *
5145
     ********************************************/
5146
    /**
5147
     * Creates a new version of a record
5148
     * (Requires support in the table)
5149
     *
5150
     * @param string $table Table name
5151
     * @param int $id Record uid to versionize
5152
     * @param string $label Version label
5153
     * @param bool $delete If TRUE, the version is created to delete the record.
5154
     * @return int|null Returns the id of the new version (if any)
5155
     * @see copyRecord()
5156
     */
5157
    public function versionizeRecord($table, $id, $label, $delete = false)
5158
    {
5159
        $id = (int)$id;
5160
        // Stop any actions if the record is marked to be deleted:
5161
        // (this can occur if IRRE elements are versionized and child elements are removed)
5162
        if ($this->isElementToBeDeleted($table, $id)) {
5163
            return null;
5164
        }
5165
        if (!BackendUtility::isTableWorkspaceEnabled($table) || $id <= 0) {
5166
            $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, SystemLogErrorClassification::USER_ERROR);
5167
            return null;
5168
        }
5169
5170
        // Fetch record with permission check
5171
        $row = $this->recordInfoWithPermissionCheck($table, $id, Permission::PAGE_SHOW);
5172
5173
        // This checks if the record can be selected which is all that a copy action requires.
5174
        if ($row === false) {
5175
            $this->newlog(
5176
                'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "' . $table . ':' . $id . '"',
5177
                SystemLogErrorClassification::USER_ERROR
5178
            );
5179
            return null;
5180
        }
5181
5182
        // Record must be online record, otherwise we would create a version of a version
5183
        if ($row['t3ver_oid'] ?? 0 > 0) {
5184
            $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (record has an online ID)!', SystemLogErrorClassification::USER_ERROR);
5185
            return null;
5186
        }
5187
5188
        // Record must not be placeholder for moving.
5189
        if (VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
5190
            $this->newlog('Record cannot be versioned because it is a placeholder for a moving operation', SystemLogErrorClassification::USER_ERROR);
5191
            return null;
5192
        }
5193
5194
        if ($delete && $this->cannotDeleteRecord($table, $id)) {
5195
            $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), SystemLogErrorClassification::USER_ERROR);
5196
            return null;
5197
        }
5198
5199
        // Set up the values to override when making a raw-copy:
5200
        $overrideArray = [
5201
            't3ver_oid' => $id,
5202
            't3ver_wsid' => $this->BE_USER->workspace,
5203
            't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5204
            't3ver_count' => 0,
5205
            't3ver_stage' => 0,
5206
            't3ver_tstamp' => 0
5207
        ];
5208
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5209
            $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5210
        }
5211
        // Checking if the record already has a version in the current workspace of the backend user
5212
        $versionRecord = ['uid' => null];
5213
        if ($this->BE_USER->workspace !== 0) {
5214
            // Look for version already in workspace:
5215
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5216
        }
5217
        // Create new version of the record and return the new uid
5218
        if (empty($versionRecord['uid'])) {
5219
            // Create raw-copy and return result:
5220
            // The information of the label to be used for the workspace record
5221
            // as well as the information whether the record shall be removed
5222
            // must be forwarded (creating remove placeholders on a workspace are
5223
            // done by copying the record and override several fields).
5224
            $workspaceOptions = [
5225
                'delete' => $delete,
5226
                'label' => $label,
5227
            ];
5228
            return $this->copyRecord_raw($table, $id, (int)$row['pid'], $overrideArray, $workspaceOptions);
5229
        }
5230
        // Reuse the existing record and return its uid
5231
        // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5232
        // did not make much sense since the information is available)
5233
        return $versionRecord['uid'];
5234
    }
5235
5236
    /**
5237
     * Swaps MM-relations for current/swap record, see version_swap()
5238
     *
5239
     * @param string $table Table for the two input records
5240
     * @param int $id Current record (about to go offline)
5241
     * @param int $swapWith Swap record (about to go online)
5242
     * @see version_swap()
5243
     */
5244
    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...
5245
    {
5246
        // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5247
        $currentRec = BackendUtility::getRecord($table, $id);
5248
        $swapRec = BackendUtility::getRecord($table, $swapWith);
5249
        $this->version_remapMMForVersionSwap_reg = [];
5250
        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5251
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5252
            $conf = $fConf['config'];
5253
            if ($this->isReferenceField($conf)) {
5254
                $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5255
                $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5256
                if ($conf['MM']) {
5257
                    /** @var RelationHandler $dbAnalysis */
5258
                    $dbAnalysis = $this->createRelationHandlerInstance();
5259
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5260
                    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

5260
                    if (!empty($dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName))) {
Loading history...
5261
                        $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5262
                    }
5263
                    /** @var RelationHandler $dbAnalysis */
5264
                    $dbAnalysis = $this->createRelationHandlerInstance();
5265
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5266
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
5267
                        $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5268
                    }
5269
                }
5270
            } elseif ($conf['type'] === 'flex') {
5271
                // Current record
5272
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5273
                    $fConf,
5274
                    $table,
5275
                    $field,
5276
                    $currentRec
5277
                );
5278
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5279
                $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5280
                if (is_array($currentValueArray)) {
5281
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5282
                }
5283
                // Swap record
5284
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5285
                    $fConf,
5286
                    $table,
5287
                    $field,
5288
                    $swapRec
5289
                );
5290
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5291
                $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5292
                if (is_array($currentValueArray)) {
5293
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5294
                }
5295
            }
5296
        }
5297
        // Execute:
5298
        $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5299
    }
5300
5301
    /**
5302
     * Callback function for traversing the FlexForm structure in relation to ...
5303
     *
5304
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
5305
     * @param array $dsConf TCA field configuration (from Data Structure XML)
5306
     * @param string $dataValue The value of the flexForm field
5307
     * @param string $dataValue_ext1 Not used.
5308
     * @param string $dataValue_ext2 Not used.
5309
     * @param string $path Path in flexforms
5310
     * @see version_remapMMForVersionSwap()
5311
     * @see checkValue_flex_procInData_travDS()
5312
     */
5313
    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...
5314
    {
5315
        // Extract parameters:
5316
        [$table, $uid, $field] = $pParams;
5317
        if ($this->isReferenceField($dsConf)) {
5318
            $allowedTables = $dsConf['type'] === 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5319
            $prependName = $dsConf['type'] === 'group' ? $dsConf['prepend_tname'] : '';
5320
            if ($dsConf['MM']) {
5321
                /** @var RelationHandler $dbAnalysis */
5322
                $dbAnalysis = $this->createRelationHandlerInstance();
5323
                $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5324
                $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5325
            }
5326
        }
5327
    }
5328
5329
    /**
5330
     * Performing the remapping operations found necessary in version_remapMMForVersionSwap()
5331
     * 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.
5332
     *
5333
     * @param string $table Table for the two input records
5334
     * @param int $id Current record (about to go offline)
5335
     * @param int $swapWith Swap record (about to go online)
5336
     * @see version_remapMMForVersionSwap()
5337
     */
5338
    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...
5339
    {
5340
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5341
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5342
                $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5343
            }
5344
        }
5345
        if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5346
            foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5347
                $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5348
            }
5349
        }
5350
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5351
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5352
                $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5353
            }
5354
        }
5355
    }
5356
5357
    /*********************************************
5358
     *
5359
     * Cmd: Helper functions
5360
     *
5361
     ********************************************/
5362
5363
    /**
5364
     * Returns an instance of DataHandler for handling local datamaps/cmdmaps
5365
     *
5366
     * @return DataHandler
5367
     */
5368
    protected function getLocalTCE()
5369
    {
5370
        $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...
5371
        $copyTCE->copyTree = $this->copyTree;
5372
        $copyTCE->enableLogging = $this->enableLogging;
5373
        // Transformations should NOT be carried out during copy
5374
        $copyTCE->dontProcessTransformations = true;
5375
        // make sure the isImporting flag is transferred, so all hooks know if
5376
        // the current process is an import process
5377
        $copyTCE->isImporting = $this->isImporting;
5378
        $copyTCE->bypassAccessCheckForRecords = $this->bypassAccessCheckForRecords;
5379
        $copyTCE->bypassWorkspaceRestrictions = $this->bypassWorkspaceRestrictions;
5380
        return $copyTCE;
5381
    }
5382
5383
    /**
5384
     * Processes the fields with references as registered during the copy process. This includes all FlexForm fields which had references.
5385
     */
5386
    public function remapListedDBRecords()
5387
    {
5388
        if (!empty($this->registerDBList)) {
5389
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5390
            foreach ($this->registerDBList as $table => $records) {
5391
                foreach ($records as $uid => $fields) {
5392
                    $newData = [];
5393
                    $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5394
                    $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5395
                    foreach ($fields as $fieldName => $value) {
5396
                        $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5397
                        switch ($conf['type']) {
5398
                            case 'group':
5399
                            case 'select':
5400
                                $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5401
                                if (is_array($vArray)) {
5402
                                    $newData[$fieldName] = implode(',', $vArray);
5403
                                }
5404
                                break;
5405
                            case 'flex':
5406
                                if ($value === 'FlexForm_reference') {
5407
                                    // This will fetch the new row for the element
5408
                                    $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5409
                                    if (is_array($origRecordRow)) {
5410
                                        BackendUtility::workspaceOL($table, $origRecordRow);
5411
                                        // Get current data structure and value array:
5412
                                        $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5413
                                            ['config' => $conf],
5414
                                            $table,
5415
                                            $fieldName,
5416
                                            $origRecordRow
5417
                                        );
5418
                                        $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5419
                                        $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5420
                                        // Do recursive processing of the XML data:
5421
                                        $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5422
                                        // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5423
                                        if (is_array($currentValueArray['data'])) {
5424
                                            $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5425
                                        }
5426
                                    }
5427
                                }
5428
                                break;
5429
                            case 'inline':
5430
                                $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5431
                                break;
5432
                            default:
5433
                                $this->logger->debug('Field type should not appear here: ' . $conf['type']);
5434
                        }
5435
                    }
5436
                    // If any fields were changed, those fields are updated!
5437
                    if (!empty($newData)) {
5438
                        $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5439
                    }
5440
                }
5441
            }
5442
        }
5443
    }
5444
5445
    /**
5446
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
5447
     *
5448
     * @param array $pParams Set of parameters in numeric array: table, uid, field
5449
     * @param array $dsConf TCA config for field (from Data Structure of course)
5450
     * @param string $dataValue Field value (from FlexForm XML)
5451
     * @param string $dataValue_ext1 Not used
5452
     * @param string $dataValue_ext2 Not used
5453
     * @return array Array where the "value" key carries the value.
5454
     * @see checkValue_flex_procInData_travDS()
5455
     * @see remapListedDBRecords()
5456
     */
5457
    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...
5458
    {
5459
        // Extract parameters:
5460
        [$table, $uid, $field] = $pParams;
5461
        // If references are set for this field, set flag so they can be corrected later:
5462
        if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5463
            $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5464
            if (is_array($vArray)) {
5465
                $dataValue = implode(',', $vArray);
5466
            }
5467
        }
5468
        // Return
5469
        return ['value' => $dataValue];
5470
    }
5471
5472
    /**
5473
     * Performs remapping of old UID values to NEW uid values for a DB reference field.
5474
     *
5475
     * @param array $conf TCA field config
5476
     * @param string $value Field value
5477
     * @param int $MM_localUid UID of local record (for MM relations - might need to change if support for FlexForms should be done!)
5478
     * @param string $table Table name
5479
     * @return array|null Returns array of items ready to implode for field content.
5480
     * @see remapListedDBRecords()
5481
     */
5482
    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...
5483
    {
5484
        // Initialize variables
5485
        // Will be set TRUE if an upgrade should be done...
5486
        $set = false;
5487
        // Allowed tables for references.
5488
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5489
        // Table name to prepend the UID
5490
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5491
        // Which tables that should possibly not be remapped
5492
        $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5493
        // Convert value to list of references:
5494
        $dbAnalysis = $this->createRelationHandlerInstance();
5495
        $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && $conf['allowNonIdValues'];
5496
        $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5497
        // Traverse those references and map IDs:
5498
        foreach ($dbAnalysis->itemArray as $k => $v) {
5499
            $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5500
            if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5501
                $dbAnalysis->itemArray[$k]['id'] = $mapID;
5502
                $set = true;
5503
            }
5504
        }
5505
        if (!empty($conf['MM'])) {
5506
            // Purge invalid items (live/version)
5507
            $dbAnalysis->purgeItemArray();
5508
            if ($dbAnalysis->isPurged()) {
5509
                $set = true;
5510
            }
5511
5512
            // If record has been versioned/copied in this process, handle invalid relations of the live record
5513
            $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5514
            $originalId = 0;
5515
            if (!empty($this->copyMappingArray_merged[$table])) {
5516
                $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5517
            }
5518
            if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5519
                $liveRelations = $this->createRelationHandlerInstance();
5520
                $liveRelations->setWorkspaceId(0);
5521
                $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5522
                // Purge invalid relations in the live workspace ("0")
5523
                $liveRelations->purgeItemArray(0);
5524
                if ($liveRelations->isPurged()) {
5525
                    $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

5525
                    $liveRelations->writeMM($conf['MM'], $liveId, /** @scrutinizer ignore-type */ $prependName);
Loading history...
5526
                }
5527
            }
5528
        }
5529
        // If a change has been done, set the new value(s)
5530
        if ($set) {
5531
            if ($conf['MM']) {
5532
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5533
            } else {
5534
                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

5534
                return $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName);
Loading history...
5535
            }
5536
        }
5537
        return null;
5538
    }
5539
5540
    /**
5541
     * Performs remapping of old UID values to NEW uid values for an inline field.
5542
     *
5543
     * @param array $conf TCA field config
5544
     * @param string $value Field value
5545
     * @param int $uid The uid of the ORIGINAL record
5546
     * @param string $table Table name
5547
     */
5548
    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...
5549
    {
5550
        $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5551
        if ($conf['foreign_table']) {
5552
            $inlineType = $this->getInlineFieldType($conf);
5553
            if ($inlineType === 'mm') {
5554
                $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5555
            } elseif ($inlineType !== false) {
5556
                /** @var RelationHandler $dbAnalysis */
5557
                $dbAnalysis = $this->createRelationHandlerInstance();
5558
                $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5559
5560
                $updatePidForRecords = [];
5561
                // Update values for specific versioned records
5562
                foreach ($dbAnalysis->itemArray as &$item) {
5563
                    $updatePidForRecords[$item['table']][] = $item['id'];
5564
                    $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5565
                    if (!empty($versionedId)) {
5566
                        $updatePidForRecords[$item['table']][] = $versionedId;
5567
                        $item['id'] = $versionedId;
5568
                    }
5569
                }
5570
5571
                // Update child records if using pointer fields ('foreign_field'):
5572
                if ($inlineType === 'field') {
5573
                    $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
5574
                }
5575
                $thePidToUpdate = null;
5576
                // If the current field is set on a page record, update the pid of related child records:
5577
                if ($table === 'pages') {
5578
                    $thePidToUpdate = $theUidToUpdate;
5579
                } elseif (isset($this->registerDBPids[$table][$uid])) {
5580
                    $thePidToUpdate = $this->registerDBPids[$table][$uid];
5581
                    $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
5582
                }
5583
5584
                // Update child records if change to pid is required
5585
                if ($thePidToUpdate && !empty($updatePidForRecords)) {
5586
                    // Ensure that only the default language page is used as PID
5587
                    $thePidToUpdate = $this->getDefaultLanguagePageId($thePidToUpdate);
5588
                    // @todo: this can probably go away
5589
                    // ensure, only live page ids are used as 'pid' values
5590
                    $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
5591
                    if ($liveId !== null) {
5592
                        $thePidToUpdate = $liveId;
5593
                    }
5594
                    $updateValues = ['pid' => $thePidToUpdate];
5595
                    foreach ($updatePidForRecords as $tableName => $uids) {
5596
                        $uids = array_map('trim', $uids);
5597
                        if (empty($tableName) || empty($uids)) {
5598
                            continue;
5599
                        }
5600
                        $conn = GeneralUtility::makeInstance(ConnectionPool::class)
5601
                            ->getConnectionForTable($tableName);
5602
                        foreach ($uids as $uid) {
0 ignored issues
show
introduced by
$uid is overwriting one of the parameters of this function.
Loading history...
5603
                            $conn->update($tableName, $updateValues, ['uid' => (int)$uid]);
5604
                        }
5605
                    }
5606
                }
5607
            }
5608
        }
5609
    }
5610
5611
    /**
5612
     * Processes the $this->remapStack at the end of copying, inserting, etc. actions.
5613
     * The remapStack takes care about the correct mapping of new and old uids in case of relational data.
5614
     */
5615
    public function processRemapStack()
5616
    {
5617
        // Processes the remap stack:
5618
        if (is_array($this->remapStack)) {
0 ignored issues
show
introduced by
The condition is_array($this->remapStack) is always true.
Loading history...
5619
            $remapFlexForms = [];
5620
            $hookPayload = [];
5621
5622
            $newValue = null;
5623
            foreach ($this->remapStack as $remapAction) {
5624
                // If no position index for the arguments was set, skip this remap action:
5625
                if (!is_array($remapAction['pos'])) {
5626
                    continue;
5627
                }
5628
                // Load values from the argument array in remapAction:
5629
                $field = $remapAction['field'];
5630
                $id = $remapAction['args'][$remapAction['pos']['id']];
5631
                $rawId = $id;
5632
                $table = $remapAction['args'][$remapAction['pos']['table']];
5633
                $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
5634
                $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
5635
                $additionalData = $remapAction['additionalData'];
5636
                // The record is new and has one or more new ids (in case of versioning/workspaces):
5637
                if (strpos($id, 'NEW') !== false) {
5638
                    // Replace NEW...-ID with real uid:
5639
                    $id = $this->substNEWwithIDs[$id];
5640
                    // If the new parent record is on a non-live workspace or versionized, it has another new id:
5641
                    if (isset($this->autoVersionIdMap[$table][$id])) {
5642
                        $id = $this->autoVersionIdMap[$table][$id];
5643
                    }
5644
                    $remapAction['args'][$remapAction['pos']['id']] = $id;
5645
                }
5646
                // Replace relations to NEW...-IDs in field value (uids of child records):
5647
                if (is_array($valueArray)) {
5648
                    foreach ($valueArray as $key => $value) {
5649
                        if (strpos($value, 'NEW') !== false) {
5650
                            if (strpos($value, '_') === false) {
5651
                                $affectedTable = $tcaFieldConf['foreign_table'];
5652
                                $prependTable = false;
5653
                            } else {
5654
                                $parts = explode('_', $value);
5655
                                $value = array_pop($parts);
5656
                                $affectedTable = implode('_', $parts);
5657
                                $prependTable = true;
5658
                            }
5659
                            $value = $this->substNEWwithIDs[$value];
5660
                            // The record is new, but was also auto-versionized and has another new id:
5661
                            if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
5662
                                $value = $this->autoVersionIdMap[$affectedTable][$value];
5663
                            }
5664
                            if ($prependTable) {
5665
                                $value = $affectedTable . '_' . $value;
5666
                            }
5667
                            // Set a hint that this was a new child record:
5668
                            $this->newRelatedIDs[$affectedTable][] = $value;
5669
                            $valueArray[$key] = $value;
5670
                        }
5671
                    }
5672
                    $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
5673
                }
5674
                // Process the arguments with the defined function:
5675
                if (!empty($remapAction['func'])) {
5676
                    $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
5677
                }
5678
                // If array is returned, check for maxitems condition, if string is returned this was already done:
5679
                if (is_array($newValue)) {
5680
                    $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
5681
                    // The reference casting is only required if
5682
                    // checkValue_group_select_processDBdata() returns an array
5683
                    $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
5684
                }
5685
                // Update in database (list of children (csv) or number of relations (foreign_field)):
5686
                if (!empty($field)) {
5687
                    $fieldArray = [$field => $newValue];
5688
                    if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
5689
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
5690
                    }
5691
                    $this->updateDB($table, $id, $fieldArray);
5692
                } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
5693
                    // Collect data to update FlexForms
5694
                    $flexFormId = $additionalData['flexFormId'];
5695
                    $flexFormPath = $additionalData['flexFormPath'];
5696
5697
                    if (!isset($remapFlexForms[$flexFormId])) {
5698
                        $remapFlexForms[$flexFormId] = [];
5699
                    }
5700
5701
                    $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
5702
                }
5703
5704
                // Collect elements that shall trigger processDatamap_afterDatabaseOperations
5705
                if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
5706
                    $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
5707
                    if (!isset($hookPayload[$table][$rawId])) {
5708
                        $hookPayload[$table][$rawId] = [
5709
                            'status' => $hookArgs['status'],
5710
                            'fieldArray' => $hookArgs['fieldArray'],
5711
                            'hookObjects' => $hookArgs['hookObjectsArr'],
5712
                        ];
5713
                    }
5714
                    $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
5715
                }
5716
            }
5717
5718
            if ($remapFlexForms) {
5719
                foreach ($remapFlexForms as $flexFormId => $modifications) {
5720
                    $this->updateFlexFormData($flexFormId, $modifications);
5721
                }
5722
            }
5723
5724
            foreach ($hookPayload as $tableName => $rawIdPayload) {
5725
                foreach ($rawIdPayload as $rawId => $payload) {
5726
                    foreach ($payload['hookObjects'] as $hookObject) {
5727
                        if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
5728
                            continue;
5729
                        }
5730
                        $hookObject->processDatamap_afterDatabaseOperations(
5731
                            $payload['status'],
5732
                            $tableName,
5733
                            $rawId,
5734
                            $payload['fieldArray'],
5735
                            $this
5736
                        );
5737
                    }
5738
                }
5739
            }
5740
        }
5741
        // Processes the remap stack actions:
5742
        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...
5743
            foreach ($this->remapStackActions as $action) {
5744
                if (isset($action['callback'], $action['arguments'])) {
5745
                    call_user_func_array($action['callback'], $action['arguments']);
5746
                }
5747
            }
5748
        }
5749
        // Processes the reference index updates of the remap stack:
5750
        foreach ($this->remapStackRefIndex as $table => $idArray) {
5751
            foreach ($idArray as $id) {
5752
                $this->updateRefIndex($table, $id);
5753
                unset($this->remapStackRefIndex[$table][$id]);
5754
            }
5755
        }
5756
        // Reset:
5757
        $this->remapStack = [];
5758
        $this->remapStackRecords = [];
5759
        $this->remapStackActions = [];
5760
        $this->remapStackRefIndex = [];
5761
    }
5762
5763
    /**
5764
     * Updates FlexForm data.
5765
     *
5766
     * @param string $flexFormId e.g. <table>:<uid>:<field>
5767
     * @param array $modifications Modifications with paths and values (e.g. 'sDEF/lDEV/field/vDEF' => 'TYPO3')
5768
     */
5769
    protected function updateFlexFormData($flexFormId, array $modifications)
5770
    {
5771
        [$table, $uid, $field] = explode(':', $flexFormId, 3);
5772
5773
        if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
5774
            $uid = $this->substNEWwithIDs[$uid];
5775
        }
5776
5777
        $record = $this->recordInfo($table, $uid, '*');
5778
5779
        if (!$table || !$uid || !$field || !is_array($record)) {
5780
            return;
5781
        }
5782
5783
        BackendUtility::workspaceOL($table, $record);
5784
5785
        // Get current data structure and value array:
5786
        $valueStructure = GeneralUtility::xml2array($record[$field]);
5787
5788
        // Do recursive processing of the XML data:
5789
        foreach ($modifications as $path => $value) {
5790
            $valueStructure['data'] = ArrayUtility::setValueByPath(
5791
                $valueStructure['data'],
5792
                $path,
5793
                $value
5794
            );
5795
        }
5796
5797
        if (is_array($valueStructure['data'])) {
5798
            // The return value should be compiled back into XML
5799
            $values = [
5800
                $field => $this->checkValue_flexArray2Xml($valueStructure, true),
5801
            ];
5802
5803
            $this->updateDB($table, $uid, $values);
5804
        }
5805
    }
5806
5807
    /**
5808
     * Triggers a remap action for a specific record.
5809
     *
5810
     * Some records are post-processed by the processRemapStack() method (e.g. IRRE children).
5811
     * This method determines whether an action/modification is executed directly to a record
5812
     * or is postponed to happen after remapping data.
5813
     *
5814
     * @param string $table Name of the table
5815
     * @param string $id Id of the record (can also be a "NEW..." string)
5816
     * @param array $callback The method to be called
5817
     * @param array $arguments The arguments to be submitted to the callback method
5818
     * @param bool $forceRemapStackActions Whether to force to use the stack
5819
     * @see processRemapStack
5820
     */
5821
    protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
5822
    {
5823
        // Check whether the affected record is marked to be remapped:
5824
        if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
5825
            call_user_func_array($callback, $arguments);
5826
        } else {
5827
            $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

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

6667
                    $this->getRecordHistoryStore()->addRecord($table, $id, /** @scrutinizer ignore-type */ $newRow, $this->correlationId);
Loading history...
6668
6669
                    if ($newVersion) {
6670
                        if ($this->enableLogging) {
6671
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6672
                            $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);
6673
                        }
6674
                    } else {
6675
                        if ($this->enableLogging) {
6676
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6677
                            $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
6678
                            $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);
6679
                        }
6680
                        // Clear cache for relevant pages:
6681
                        $this->registerRecordIdForPageCacheClearing($table, $id);
6682
                    }
6683
                    return $id;
6684
                }
6685
                if ($this->enableLogging) {
6686
                    $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

6686
                    $this->log($table, /** @scrutinizer ignore-type */ $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
Loading history...
6687
                }
6688
            }
6689
        }
6690
        return null;
6691
    }
6692
6693
    /**
6694
     * Checking stored record to see if the written values are properly updated.
6695
     *
6696
     * @param string $table Record table name
6697
     * @param int $id Record uid
6698
     * @param array $fieldArray Array of field=>value pairs to insert/update
6699
     * @param string $action Action, for logging only.
6700
     * @return array|null Selected row
6701
     * @see insertDB()
6702
     * @see updateDB()
6703
     */
6704
    public function checkStoredRecord($table, $id, $fieldArray, $action)
6705
    {
6706
        $id = (int)$id;
6707
        if (is_array($GLOBALS['TCA'][$table]) && $id) {
6708
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6709
            $queryBuilder->getRestrictions()->removeAll();
6710
6711
            $row = $queryBuilder
6712
                ->select('*')
6713
                ->from($table)
6714
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6715
                ->execute()
6716
                ->fetch();
6717
6718
            if (!empty($row)) {
6719
                // Traverse array of values that was inserted into the database and compare with the actually stored value:
6720
                $errors = [];
6721
                foreach ($fieldArray as $key => $value) {
6722
                    if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
6723
                        if (is_float($row[$key])) {
6724
                            // if the database returns the value as double, compare it as double
6725
                            if ((double)$value !== (double)$row[$key]) {
6726
                                $errors[] = $key;
6727
                            }
6728
                        } else {
6729
                            $dbType = $GLOBALS['TCA'][$table]['columns'][$key]['config']['dbType'] ?? false;
6730
                            if ($dbType === 'datetime' || $dbType === 'time') {
6731
                                $row[$key] = $this->normalizeTimeFormat($table, $row[$key], $dbType);
6732
                            }
6733
                            if ((string)$value !== (string)$row[$key]) {
6734
                                // The is_numeric check catches cases where we want to store a float/double value
6735
                                // and database returns the field as a string with the least required amount of
6736
                                // significant digits, i.e. "0.00" being saved and "0" being read back.
6737
                                if (is_numeric($value) && is_numeric($row[$key])) {
6738
                                    if ((double)$value === (double)$row[$key]) {
6739
                                        continue;
6740
                                    }
6741
                                }
6742
                                $errors[] = $key;
6743
                            }
6744
                        }
6745
                    }
6746
                }
6747
                // Set log message if there were fields with unmatching values:
6748
                if (!empty($errors)) {
6749
                    $message = sprintf(
6750
                        '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.',
6751
                        $id,
6752
                        $table,
6753
                        implode(', ', $errors)
6754
                    );
6755
                    $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

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

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

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