Completed
Push — master ( 01c6cd...a6a7a5 )
by
unknown
38:09 queued 22:36
created

DataHandler::applyDefaultsForFieldArray()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

1532
                    $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...
1533
                }
1534
                return $res;
1535
            }
1536
            if ($status === 'update') {
1537
                // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1538
                $onlyAllowedTables = $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1539
                if ($onlyAllowedTables) {
1540
                    // use the real page id (default language)
1541
                    $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

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

1542
                    $theWrongTables = $this->doesPageHaveUnallowedTables($recordId, /** @scrutinizer ignore-type */ $value);
Loading history...
1543
                    if ($theWrongTables) {
1544
                        if ($this->enableLogging) {
1545
                            $propArr = $this->getRecordProperties($table, $id);
1546
                            $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']);
1547
                        }
1548
                        return $res;
1549
                    }
1550
                }
1551
            }
1552
        }
1553
1554
        $curValue = null;
1555
        if ((int)$id !== 0) {
1556
            // Get current value:
1557
            $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

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

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

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

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

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

3230
        BackendUtility::workspaceOL($table, /** @scrutinizer ignore-type */ $row, $this->BE_USER->workspace);
Loading history...
3231
        $row = BackendUtility::purgeComputedPropertiesFromRecord($row);
3232
3233
        // Initializing:
3234
        $theNewID = StringUtility::getUniqueId('NEW');
3235
        $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3236
        $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3237
        // Getting "copy-after" fields if applicable:
3238
        $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

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

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

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

3687
                    $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3688
                }
3689
            }
3690
            $purgeItems = true;
3691
        }
3692
3693
        if ($purgeItems || $mmTable) {
3694
            $dbAnalysis->purgeItemArray();
3695
            $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

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

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

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

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

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

3853
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, /** @scrutinizer ignore-type */ $destPid);
Loading history...
3854
        // Get the localized records to be copied
3855
        $l10nRecords = $queryBuilder->execute()->fetchAll();
3856
        if (is_array($l10nRecords)) {
3857
            $localizedDestPids = [];
3858
            // If $destPid < 0, then it is the uid of the original language record we are inserting after
3859
            if ($destPid < 0) {
3860
                // Get the localized records of the record we are inserting after
3861
                $queryBuilder->setParameter('pointer', abs($destPid), \PDO::PARAM_INT);
3862
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
3863
                // Index the localized record uids by language
3864
                if (is_array($destL10nRecords)) {
3865
                    foreach ($destL10nRecords as $record) {
3866
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
3867
                    }
3868
                }
3869
            }
3870
            $languageSourceMap = [
3871
                $uid => $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
3872
            ];
3873
            // Copy the localized records after the corresponding localizations of the destination record
3874
            foreach ($l10nRecords as $record) {
3875
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
3876
                if ($localizedDestPid < 0) {
3877
                    $newUid = $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
3878
                } else {
3879
                    $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

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

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

4222
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
4223
                }
4224
            }
4225
        }
4226
        // Move the records
4227
        if (isset($dbAnalysis)) {
4228
            // Moving records to a positive destination will insert each
4229
            // record at the beginning, thus the order is reversed here:
4230
            foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4231
                $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

4231
                $this->moveRecord($v['table'], $v['id'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4232
            }
4233
        }
4234
    }
4235
4236
    /**
4237
     * Find l10n-overlay records and perform the requested move action for these records.
4238
     *
4239
     * @param string $table Record Table
4240
     * @param string $uid Record UID
4241
     * @param string $destPid Position to move to
4242
     * @param string $originalRecordDestinationPid Position to move the original record to
4243
     */
4244
    public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4245
    {
4246
        // There's no need to perform this for non-localizable tables
4247
        if (!BackendUtility::isTableLocalizable($table)) {
4248
            return;
4249
        }
4250
4251
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4252
        $queryBuilder->getRestrictions()
4253
            ->removeAll()
4254
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4255
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
4256
4257
        $queryBuilder->select('*')
4258
            ->from($table)
4259
            ->where(
4260
                $queryBuilder->expr()->eq(
4261
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
4262
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
4263
                )
4264
            );
4265
4266
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
4267
            $queryBuilder->andWhere(
4268
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
4269
            );
4270
        }
4271
4272
        $l10nRecords = $queryBuilder->execute()->fetchAll();
4273
        if (is_array($l10nRecords)) {
4274
            $localizedDestPids = [];
4275
            // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4276
            if ($originalRecordDestinationPid < 0) {
4277
                // Get the localized records of the record we are inserting after
4278
                $queryBuilder->setParameter('pointer', abs($originalRecordDestinationPid), \PDO::PARAM_INT);
4279
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
4280
                // Index the localized record uids by language
4281
                if (is_array($destL10nRecords)) {
4282
                    foreach ($destL10nRecords as $record) {
4283
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4284
                    }
4285
                }
4286
            }
4287
            // Move the localized records after the corresponding localizations of the destination record
4288
            foreach ($l10nRecords as $record) {
4289
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4290
                if ($localizedDestPid < 0) {
4291
                    $this->moveRecord($table, $record['uid'], $localizedDestPid);
4292
                } else {
4293
                    $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

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

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

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

5051
                $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...
5052
            }
5053
        } else {
5054
            // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5055
            $result = true;
5056
        }
5057
        return $result;
5058
    }
5059
5060
    /**
5061
     * Before a record is deleted, check if it has references such as inline type or MM references.
5062
     * If so, set these child records also to be deleted.
5063
     *
5064
     * @param string $table Record Table
5065
     * @param string $uid Record UID
5066
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5067
     * @see deleteRecord()
5068
     */
5069
    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...
5070
    {
5071
        $conf = $GLOBALS['TCA'][$table]['columns'];
5072
        $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

5072
        $row = BackendUtility::getRecord($table, /** @scrutinizer ignore-type */ $uid, '*', '', false);
Loading history...
5073
        if (empty($row)) {
5074
            return;
5075
        }
5076
        foreach ($row as $field => $value) {
5077
            $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5078
        }
5079
    }
5080
5081
    /**
5082
     * Process fields of a record to be deleted and search for special handling, like
5083
     * inline type, MM records, etc.
5084
     *
5085
     * @param string $table Record Table
5086
     * @param string $uid Record UID
5087
     * @param string $field Record field
5088
     * @param string $value Record field value
5089
     * @param array $conf TCA configuration of current field
5090
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5091
     * @see deleteRecord()
5092
     */
5093
    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...
5094
    {
5095
        if ($conf['type'] === 'inline') {
5096
            $foreign_table = $conf['foreign_table'];
5097
            if ($foreign_table) {
5098
                $inlineType = $this->getInlineFieldType($conf);
5099
                if ($inlineType === 'list' || $inlineType === 'field') {
5100
                    /** @var RelationHandler $dbAnalysis */
5101
                    $dbAnalysis = $this->createRelationHandlerInstance();
5102
                    $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

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

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

5559
                    $liveRelations->writeMM($conf['MM'], $liveId, /** @scrutinizer ignore-type */ $prependName);
Loading history...
5560
                }
5561
            }
5562
        }
5563
        // If a change has been done, set the new value(s)
5564
        if ($set) {
5565
            if ($conf['MM']) {
5566
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5567
            } else {
5568
                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

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

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

6701
                    $this->getRecordHistoryStore()->addRecord($table, $id, /** @scrutinizer ignore-type */ $newRow, $this->correlationId);
Loading history...
6702
6703
                    if ($newVersion) {
6704
                        if ($this->enableLogging) {
6705
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6706
                            $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);
6707
                        }
6708
                    } else {
6709
                        if ($this->enableLogging) {
6710
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6711
                            $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
6712
                            $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);
6713
                        }
6714
                        // Clear cache for relevant pages:
6715
                        $this->registerRecordIdForPageCacheClearing($table, $id);
6716
                    }
6717
                    return $id;
6718
                }
6719
                if ($this->enableLogging) {
6720
                    $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

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

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

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

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