Completed
Push — master ( 8130f4...4f4b17 )
by
unknown
17:27
created

DataHandler::prependLabel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

$answer = 42;

$correct = false;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3866
        $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

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

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

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

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

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

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

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

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

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
5121
    }
5122
5123
    /**
5124
     * Determines whether a record can be undeleted.
5125
     *
5126
     * @param string $table Table name of the record
5127
     * @param int $uid uid of the record
5128
     * @return bool Whether the record can be undeleted
5129
     * @internal should only be used from within DataHandler
5130
     */
5131
    public function isRecordUndeletable($table, $uid)
5132
    {
5133
        $result = false;
5134
        $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5135
        if ($record['pid']) {
5136
            $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5137
            // The page containing the record is not deleted, thus the record can be undeleted:
5138
            if (!$page['deleted']) {
5139
                $result = true;
5140
            } else {
5141
                $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

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

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

5194
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
5195
                    $dbAnalysis->undeleteRecord = true;
5196
5197
                    $enableCascadingDelete = true;
5198
                    // non type save comparison is intended!
5199
                    if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5200
                        $enableCascadingDelete = false;
5201
                    }
5202
5203
                    // Walk through the items and remove them
5204
                    foreach ($dbAnalysis->itemArray as $v) {
5205
                        if (!$undeleteRecord) {
5206
                            if ($enableCascadingDelete) {
5207
                                $this->deleteAction($v['table'], $v['id']);
5208
                            }
5209
                        } else {
5210
                            $this->undeleteRecord($v['table'], $v['id']);
5211
                        }
5212
                    }
5213
                }
5214
            }
5215
        } elseif ($this->isReferenceField($conf)) {
5216
            $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5217
            $dbAnalysis = $this->createRelationHandlerInstance();
5218
            $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5219
            foreach ($dbAnalysis->itemArray as $v) {
5220
                $this->updateRefIndexStack[$table][$uid][] = [$v['table'], $v['id']];
5221
            }
5222
        }
5223
    }
5224
5225
    /**
5226
     * Find l10n-overlay records and perform the requested delete action for these records.
5227
     *
5228
     * @param string $table Record Table
5229
     * @param string $uid Record UID
5230
     * @internal should only be used from within DataHandler
5231
     */
5232
    public function deleteL10nOverlayRecords($table, $uid)
5233
    {
5234
        // Check whether table can be localized
5235
        if (!BackendUtility::isTableLocalizable($table)) {
5236
            return;
5237
        }
5238
5239
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5240
        $queryBuilder->getRestrictions()
5241
            ->removeAll()
5242
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5243
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
5244
5245
        $queryBuilder->select('*')
5246
            ->from($table)
5247
            ->where(
5248
                $queryBuilder->expr()->eq(
5249
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5250
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5251
                )
5252
            );
5253
5254
        $result = $queryBuilder->execute();
5255
        while ($record = $result->fetch()) {
5256
            // Ignore workspace delete placeholders. Those records have been marked for
5257
            // deletion before - deleting them again in a workspace would revert that state.
5258
            if ((int)$this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5259
                BackendUtility::workspaceOL($table, $record, $this->BE_USER->workspace);
5260
                if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5261
                    continue;
5262
                }
5263
            }
5264
            $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5265
        }
5266
    }
5267
5268
    /*********************************************
5269
     *
5270
     * Cmd: Versioning
5271
     *
5272
     ********************************************/
5273
    /**
5274
     * Creates a new version of a record
5275
     * (Requires support in the table)
5276
     *
5277
     * @param string $table Table name
5278
     * @param int $id Record uid to versionize
5279
     * @param string $label Version label
5280
     * @param bool $delete If TRUE, the version is created to delete the record.
5281
     * @return int|null Returns the id of the new version (if any)
5282
     * @see copyRecord()
5283
     * @internal should only be used from within DataHandler
5284
     */
5285
    public function versionizeRecord($table, $id, $label, $delete = false)
5286
    {
5287
        $id = (int)$id;
5288
        // Stop any actions if the record is marked to be deleted:
5289
        // (this can occur if IRRE elements are versionized and child elements are removed)
5290
        if ($this->isElementToBeDeleted($table, $id)) {
5291
            return null;
5292
        }
5293
        if (!BackendUtility::isTableWorkspaceEnabled($table) || $id <= 0) {
5294
            $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, SystemLogErrorClassification::USER_ERROR);
5295
            return null;
5296
        }
5297
5298
        // Fetch record with permission check
5299
        $row = $this->recordInfoWithPermissionCheck($table, $id, Permission::PAGE_SHOW);
5300
5301
        // This checks if the record can be selected which is all that a copy action requires.
5302
        if ($row === false) {
5303
            $this->newlog(
5304
                'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "' . $table . ':' . $id . '"',
5305
                SystemLogErrorClassification::USER_ERROR
5306
            );
5307
            return null;
5308
        }
5309
5310
        // Record must be online record, otherwise we would create a version of a version
5311
        if ($row['t3ver_oid'] ?? 0 > 0) {
5312
            $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (record has an online ID)!', SystemLogErrorClassification::USER_ERROR);
5313
            return null;
5314
        }
5315
5316
        // Record must not be placeholder for moving.
5317
        if (VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
5318
            $this->newlog('Record cannot be versioned because it is a placeholder for a moving operation', SystemLogErrorClassification::USER_ERROR);
5319
            return null;
5320
        }
5321
5322
        if ($delete && $this->cannotDeleteRecord($table, $id)) {
5323
            $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), SystemLogErrorClassification::USER_ERROR);
5324
            return null;
5325
        }
5326
5327
        // Set up the values to override when making a raw-copy:
5328
        $overrideArray = [
5329
            't3ver_oid' => $id,
5330
            't3ver_wsid' => $this->BE_USER->workspace,
5331
            't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5332
            't3ver_count' => 0,
5333
            't3ver_stage' => 0,
5334
            't3ver_tstamp' => 0
5335
        ];
5336
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5337
            $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5338
        }
5339
        // Checking if the record already has a version in the current workspace of the backend user
5340
        $versionRecord = ['uid' => null];
5341
        if ($this->BE_USER->workspace !== 0) {
5342
            // Look for version already in workspace:
5343
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5344
        }
5345
        // Create new version of the record and return the new uid
5346
        if (empty($versionRecord['uid'])) {
5347
            // Create raw-copy and return result:
5348
            // The information of the label to be used for the workspace record
5349
            // as well as the information whether the record shall be removed
5350
            // must be forwarded (creating remove placeholders on a workspace are
5351
            // done by copying the record and override several fields).
5352
            $workspaceOptions = [
5353
                'delete' => $delete,
5354
                'label' => $label,
5355
            ];
5356
            return $this->copyRecord_raw($table, $id, (int)$row['pid'], $overrideArray, $workspaceOptions);
5357
        }
5358
        // Reuse the existing record and return its uid
5359
        // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5360
        // did not make much sense since the information is available)
5361
        return $versionRecord['uid'];
5362
    }
5363
5364
    /**
5365
     * Swaps MM-relations for current/swap record, see version_swap()
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_swap()
5371
     * @internal should only be used from within DataHandler
5372
     */
5373
    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...
5374
    {
5375
        // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5376
        $currentRec = BackendUtility::getRecord($table, $id);
5377
        $swapRec = BackendUtility::getRecord($table, $swapWith);
5378
        $this->version_remapMMForVersionSwap_reg = [];
5379
        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5380
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5381
            $conf = $fConf['config'];
5382
            if ($this->isReferenceField($conf)) {
5383
                $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5384
                $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5385
                if ($conf['MM']) {
5386
                    /** @var RelationHandler $dbAnalysis */
5387
                    $dbAnalysis = $this->createRelationHandlerInstance();
5388
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5389
                    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

5389
                    if (!empty($dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName))) {
Loading history...
5390
                        $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5391
                    }
5392
                    /** @var RelationHandler $dbAnalysis */
5393
                    $dbAnalysis = $this->createRelationHandlerInstance();
5394
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5395
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
5396
                        $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5397
                    }
5398
                }
5399
            } elseif ($conf['type'] === 'flex') {
5400
                // Current record
5401
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5402
                    $fConf,
5403
                    $table,
5404
                    $field,
5405
                    $currentRec
5406
                );
5407
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5408
                $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5409
                if (is_array($currentValueArray)) {
5410
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5411
                }
5412
                // Swap record
5413
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5414
                    $fConf,
5415
                    $table,
5416
                    $field,
5417
                    $swapRec
5418
                );
5419
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5420
                $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5421
                if (is_array($currentValueArray)) {
5422
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5423
                }
5424
            }
5425
        }
5426
        // Execute:
5427
        $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5428
    }
5429
5430
    /**
5431
     * Callback function for traversing the FlexForm structure in relation to ...
5432
     *
5433
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
5434
     * @param array $dsConf TCA field configuration (from Data Structure XML)
5435
     * @param string $dataValue The value of the flexForm field
5436
     * @param string $dataValue_ext1 Not used.
5437
     * @param string $dataValue_ext2 Not used.
5438
     * @param string $path Path in flexforms
5439
     * @see version_remapMMForVersionSwap()
5440
     * @see checkValue_flex_procInData_travDS()
5441
     * @internal should only be used from within DataHandler
5442
     */
5443
    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...
5444
    {
5445
        // Extract parameters:
5446
        [$table, $uid, $field] = $pParams;
5447
        if ($this->isReferenceField($dsConf)) {
5448
            $allowedTables = $dsConf['type'] === 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5449
            $prependName = $dsConf['type'] === 'group' ? $dsConf['prepend_tname'] : '';
5450
            if ($dsConf['MM']) {
5451
                /** @var RelationHandler $dbAnalysis */
5452
                $dbAnalysis = $this->createRelationHandlerInstance();
5453
                $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5454
                $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5455
            }
5456
        }
5457
    }
5458
5459
    /**
5460
     * Performing the remapping operations found necessary in version_remapMMForVersionSwap()
5461
     * 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.
5462
     *
5463
     * @param string $table Table for the two input records
5464
     * @param int $id Current record (about to go offline)
5465
     * @param int $swapWith Swap record (about to go online)
5466
     * @see version_remapMMForVersionSwap()
5467
     * @internal should only be used from within DataHandler
5468
     */
5469
    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...
5470
    {
5471
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5472
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5473
                $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5474
            }
5475
        }
5476
        if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5477
            foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5478
                $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5479
            }
5480
        }
5481
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5482
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5483
                $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5484
            }
5485
        }
5486
    }
5487
5488
    /*********************************************
5489
     *
5490
     * Cmd: Helper functions
5491
     *
5492
     ********************************************/
5493
5494
    /**
5495
     * Returns an instance of DataHandler for handling local datamaps/cmdmaps
5496
     *
5497
     * @return DataHandler
5498
     */
5499
    protected function getLocalTCE()
5500
    {
5501
        $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...
5502
        $copyTCE->copyTree = $this->copyTree;
5503
        $copyTCE->enableLogging = $this->enableLogging;
5504
        // Transformations should NOT be carried out during copy
5505
        $copyTCE->dontProcessTransformations = true;
5506
        // make sure the isImporting flag is transferred, so all hooks know if
5507
        // the current process is an import process
5508
        $copyTCE->isImporting = $this->isImporting;
5509
        $copyTCE->bypassAccessCheckForRecords = $this->bypassAccessCheckForRecords;
5510
        $copyTCE->bypassWorkspaceRestrictions = $this->bypassWorkspaceRestrictions;
5511
        return $copyTCE;
5512
    }
5513
5514
    /**
5515
     * Processes the fields with references as registered during the copy process. This includes all FlexForm fields which had references.
5516
     * @internal should only be used from within DataHandler
5517
     */
5518
    public function remapListedDBRecords()
5519
    {
5520
        if (!empty($this->registerDBList)) {
5521
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5522
            foreach ($this->registerDBList as $table => $records) {
5523
                foreach ($records as $uid => $fields) {
5524
                    $newData = [];
5525
                    $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5526
                    $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5527
                    foreach ($fields as $fieldName => $value) {
5528
                        $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5529
                        switch ($conf['type']) {
5530
                            case 'group':
5531
                            case 'select':
5532
                                $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5533
                                if (is_array($vArray)) {
5534
                                    $newData[$fieldName] = implode(',', $vArray);
5535
                                }
5536
                                break;
5537
                            case 'flex':
5538
                                if ($value === 'FlexForm_reference') {
5539
                                    // This will fetch the new row for the element
5540
                                    $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5541
                                    if (is_array($origRecordRow)) {
5542
                                        BackendUtility::workspaceOL($table, $origRecordRow);
5543
                                        // Get current data structure and value array:
5544
                                        $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5545
                                            ['config' => $conf],
5546
                                            $table,
5547
                                            $fieldName,
5548
                                            $origRecordRow
5549
                                        );
5550
                                        $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5551
                                        $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5552
                                        // Do recursive processing of the XML data:
5553
                                        $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5554
                                        // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5555
                                        if (is_array($currentValueArray['data'])) {
5556
                                            $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5557
                                        }
5558
                                    }
5559
                                }
5560
                                break;
5561
                            case 'inline':
5562
                                $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5563
                                break;
5564
                            default:
5565
                                $this->logger->debug('Field type should not appear here: ' . $conf['type']);
5566
                        }
5567
                    }
5568
                    // If any fields were changed, those fields are updated!
5569
                    if (!empty($newData)) {
5570
                        $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5571
                    }
5572
                }
5573
            }
5574
        }
5575
    }
5576
5577
    /**
5578
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
5579
     *
5580
     * @param array $pParams Set of parameters in numeric array: table, uid, field
5581
     * @param array $dsConf TCA config for field (from Data Structure of course)
5582
     * @param string $dataValue Field value (from FlexForm XML)
5583
     * @param string $dataValue_ext1 Not used
5584
     * @param string $dataValue_ext2 Not used
5585
     * @return array Array where the "value" key carries the value.
5586
     * @see checkValue_flex_procInData_travDS()
5587
     * @see remapListedDBRecords()
5588
     * @internal should only be used from within DataHandler
5589
     */
5590
    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...
5591
    {
5592
        // Extract parameters:
5593
        [$table, $uid, $field] = $pParams;
5594
        // If references are set for this field, set flag so they can be corrected later:
5595
        if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5596
            $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5597
            if (is_array($vArray)) {
5598
                $dataValue = implode(',', $vArray);
5599
            }
5600
        }
5601
        // Return
5602
        return ['value' => $dataValue];
5603
    }
5604
5605
    /**
5606
     * Performs remapping of old UID values to NEW uid values for a DB reference field.
5607
     *
5608
     * @param array $conf TCA field config
5609
     * @param string $value Field value
5610
     * @param int $MM_localUid UID of local record (for MM relations - might need to change if support for FlexForms should be done!)
5611
     * @param string $table Table name
5612
     * @return array|null Returns array of items ready to implode for field content.
5613
     * @see remapListedDBRecords()
5614
     * @internal should only be used from within DataHandler
5615
     */
5616
    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...
5617
    {
5618
        // Initialize variables
5619
        // Will be set TRUE if an upgrade should be done...
5620
        $set = false;
5621
        // Allowed tables for references.
5622
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5623
        // Table name to prepend the UID
5624
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5625
        // Which tables that should possibly not be remapped
5626
        $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5627
        // Convert value to list of references:
5628
        $dbAnalysis = $this->createRelationHandlerInstance();
5629
        $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && $conf['allowNonIdValues'];
5630
        $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5631
        // Traverse those references and map IDs:
5632
        foreach ($dbAnalysis->itemArray as $k => $v) {
5633
            $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5634
            if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5635
                $dbAnalysis->itemArray[$k]['id'] = $mapID;
5636
                $set = true;
5637
            }
5638
        }
5639
        if (!empty($conf['MM'])) {
5640
            // Purge invalid items (live/version)
5641
            $dbAnalysis->purgeItemArray();
5642
            if ($dbAnalysis->isPurged()) {
5643
                $set = true;
5644
            }
5645
5646
            // If record has been versioned/copied in this process, handle invalid relations of the live record
5647
            $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5648
            $originalId = 0;
5649
            if (!empty($this->copyMappingArray_merged[$table])) {
5650
                $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5651
            }
5652
            if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5653
                $liveRelations = $this->createRelationHandlerInstance();
5654
                $liveRelations->setWorkspaceId(0);
5655
                $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5656
                // Purge invalid relations in the live workspace ("0")
5657
                $liveRelations->purgeItemArray(0);
5658
                if ($liveRelations->isPurged()) {
5659
                    $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

5659
                    $liveRelations->writeMM($conf['MM'], $liveId, /** @scrutinizer ignore-type */ $prependName);
Loading history...
5660
                }
5661
            }
5662
        }
5663
        // If a change has been done, set the new value(s)
5664
        if ($set) {
5665
            if ($conf['MM']) {
5666
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5667
            } else {
5668
                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

5668
                return $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName);
Loading history...
5669
            }
5670
        }
5671
        return null;
5672
    }
5673
5674
    /**
5675
     * Performs remapping of old UID values to NEW uid values for an inline field.
5676
     *
5677
     * @param array $conf TCA field config
5678
     * @param string $value Field value
5679
     * @param int $uid The uid of the ORIGINAL record
5680
     * @param string $table Table name
5681
     * @internal should only be used from within DataHandler
5682
     */
5683
    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...
5684
    {
5685
        $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5686
        if ($conf['foreign_table']) {
5687
            $inlineType = $this->getInlineFieldType($conf);
5688
            if ($inlineType === 'mm') {
5689
                $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5690
            } elseif ($inlineType !== false) {
5691
                /** @var RelationHandler $dbAnalysis */
5692
                $dbAnalysis = $this->createRelationHandlerInstance();
5693
                $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5694
5695
                $updatePidForRecords = [];
5696
                // Update values for specific versioned records
5697
                foreach ($dbAnalysis->itemArray as &$item) {
5698
                    $updatePidForRecords[$item['table']][] = $item['id'];
5699
                    $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5700
                    if ($versionedId !== null) {
5701
                        $updatePidForRecords[$item['table']][] = $versionedId;
5702
                        $item['id'] = $versionedId;
5703
                    }
5704
                }
5705
5706
                // Update child records if using pointer fields ('foreign_field'):
5707
                if ($inlineType === 'field') {
5708
                    $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
5709
                }
5710
                $thePidToUpdate = null;
5711
                // If the current field is set on a page record, update the pid of related child records:
5712
                if ($table === 'pages') {
5713
                    $thePidToUpdate = $theUidToUpdate;
5714
                } elseif (isset($this->registerDBPids[$table][$uid])) {
5715
                    $thePidToUpdate = $this->registerDBPids[$table][$uid];
5716
                    $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
5717
                }
5718
5719
                // Update child records if change to pid is required
5720
                if ($thePidToUpdate && !empty($updatePidForRecords)) {
5721
                    // Ensure that only the default language page is used as PID
5722
                    $thePidToUpdate = $this->getDefaultLanguagePageId($thePidToUpdate);
5723
                    // @todo: this can probably go away
5724
                    // ensure, only live page ids are used as 'pid' values
5725
                    $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
5726
                    if ($liveId !== null) {
5727
                        $thePidToUpdate = $liveId;
5728
                    }
5729
                    $updateValues = ['pid' => $thePidToUpdate];
5730
                    foreach ($updatePidForRecords as $tableName => $uids) {
5731
                        if (empty($tableName) || empty($uids)) {
5732
                            continue;
5733
                        }
5734
                        $conn = GeneralUtility::makeInstance(ConnectionPool::class)
5735
                            ->getConnectionForTable($tableName);
5736
                        foreach ($uids as $updateUid) {
5737
                            $conn->update($tableName, $updateValues, ['uid' => $updateUid]);
5738
                        }
5739
                    }
5740
                }
5741
            }
5742
        }
5743
    }
5744
5745
    /**
5746
     * Processes the $this->remapStack at the end of copying, inserting, etc. actions.
5747
     * The remapStack takes care about the correct mapping of new and old uids in case of relational data.
5748
     * @internal should only be used from within DataHandler
5749
     */
5750
    public function processRemapStack()
5751
    {
5752
        // Processes the remap stack:
5753
        if (is_array($this->remapStack)) {
0 ignored issues
show
introduced by
The condition is_array($this->remapStack) is always true.
Loading history...
5754
            $remapFlexForms = [];
5755
            $hookPayload = [];
5756
5757
            $newValue = null;
5758
            foreach ($this->remapStack as $remapAction) {
5759
                // If no position index for the arguments was set, skip this remap action:
5760
                if (!is_array($remapAction['pos'])) {
5761
                    continue;
5762
                }
5763
                // Load values from the argument array in remapAction:
5764
                $field = $remapAction['field'];
5765
                $id = $remapAction['args'][$remapAction['pos']['id']];
5766
                $rawId = $id;
5767
                $table = $remapAction['args'][$remapAction['pos']['table']];
5768
                $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
5769
                $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
5770
                $additionalData = $remapAction['additionalData'];
5771
                // The record is new and has one or more new ids (in case of versioning/workspaces):
5772
                if (strpos($id, 'NEW') !== false) {
5773
                    // Replace NEW...-ID with real uid:
5774
                    $id = $this->substNEWwithIDs[$id];
5775
                    // If the new parent record is on a non-live workspace or versionized, it has another new id:
5776
                    if (isset($this->autoVersionIdMap[$table][$id])) {
5777
                        $id = $this->autoVersionIdMap[$table][$id];
5778
                    }
5779
                    $remapAction['args'][$remapAction['pos']['id']] = $id;
5780
                }
5781
                // Replace relations to NEW...-IDs in field value (uids of child records):
5782
                if (is_array($valueArray)) {
5783
                    foreach ($valueArray as $key => $value) {
5784
                        if (strpos($value, 'NEW') !== false) {
5785
                            if (strpos($value, '_') === false) {
5786
                                $affectedTable = $tcaFieldConf['foreign_table'];
5787
                                $prependTable = false;
5788
                            } else {
5789
                                $parts = explode('_', $value);
5790
                                $value = array_pop($parts);
5791
                                $affectedTable = implode('_', $parts);
5792
                                $prependTable = true;
5793
                            }
5794
                            $value = $this->substNEWwithIDs[$value];
5795
                            // The record is new, but was also auto-versionized and has another new id:
5796
                            if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
5797
                                $value = $this->autoVersionIdMap[$affectedTable][$value];
5798
                            }
5799
                            if ($prependTable) {
5800
                                $value = $affectedTable . '_' . $value;
5801
                            }
5802
                            // Set a hint that this was a new child record:
5803
                            $this->newRelatedIDs[$affectedTable][] = $value;
5804
                            $valueArray[$key] = $value;
5805
                        }
5806
                    }
5807
                    $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
5808
                }
5809
                // Process the arguments with the defined function:
5810
                if (!empty($remapAction['func'])) {
5811
                    $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
5812
                }
5813
                // If array is returned, check for maxitems condition, if string is returned this was already done:
5814
                if (is_array($newValue)) {
5815
                    $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
5816
                    // The reference casting is only required if
5817
                    // checkValue_group_select_processDBdata() returns an array
5818
                    $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
5819
                }
5820
                // Update in database (list of children (csv) or number of relations (foreign_field)):
5821
                if (!empty($field)) {
5822
                    $fieldArray = [$field => $newValue];
5823
                    if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
5824
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
5825
                    }
5826
                    $this->updateDB($table, $id, $fieldArray);
5827
                } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
5828
                    // Collect data to update FlexForms
5829
                    $flexFormId = $additionalData['flexFormId'];
5830
                    $flexFormPath = $additionalData['flexFormPath'];
5831
5832
                    if (!isset($remapFlexForms[$flexFormId])) {
5833
                        $remapFlexForms[$flexFormId] = [];
5834
                    }
5835
5836
                    $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
5837
                }
5838
5839
                // Collect elements that shall trigger processDatamap_afterDatabaseOperations
5840
                if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
5841
                    $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
5842
                    if (!isset($hookPayload[$table][$rawId])) {
5843
                        $hookPayload[$table][$rawId] = [
5844
                            'status' => $hookArgs['status'],
5845
                            'fieldArray' => $hookArgs['fieldArray'],
5846
                            'hookObjects' => $hookArgs['hookObjectsArr'],
5847
                        ];
5848
                    }
5849
                    $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
5850
                }
5851
            }
5852
5853
            if ($remapFlexForms) {
5854
                foreach ($remapFlexForms as $flexFormId => $modifications) {
5855
                    $this->updateFlexFormData($flexFormId, $modifications);
5856
                }
5857
            }
5858
5859
            foreach ($hookPayload as $tableName => $rawIdPayload) {
5860
                foreach ($rawIdPayload as $rawId => $payload) {
5861
                    foreach ($payload['hookObjects'] as $hookObject) {
5862
                        if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
5863
                            continue;
5864
                        }
5865
                        $hookObject->processDatamap_afterDatabaseOperations(
5866
                            $payload['status'],
5867
                            $tableName,
5868
                            $rawId,
5869
                            $payload['fieldArray'],
5870
                            $this
5871
                        );
5872
                    }
5873
                }
5874
            }
5875
        }
5876
        // Processes the remap stack actions:
5877
        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...
5878
            foreach ($this->remapStackActions as $action) {
5879
                if (isset($action['callback'], $action['arguments'])) {
5880
                    call_user_func_array($action['callback'], $action['arguments']);
5881
                }
5882
            }
5883
        }
5884
        // Processes the reference index updates of the remap stack:
5885
        foreach ($this->remapStackRefIndex as $table => $idArray) {
5886
            foreach ($idArray as $id) {
5887
                $this->updateRefIndex($table, $id);
5888
                unset($this->remapStackRefIndex[$table][$id]);
5889
            }
5890
        }
5891
        // Reset:
5892
        $this->remapStack = [];
5893
        $this->remapStackRecords = [];
5894
        $this->remapStackActions = [];
5895
        $this->remapStackRefIndex = [];
5896
    }
5897
5898
    /**
5899
     * Updates FlexForm data.
5900
     *
5901
     * @param string $flexFormId e.g. <table>:<uid>:<field>
5902
     * @param array $modifications Modifications with paths and values (e.g. 'sDEF/lDEV/field/vDEF' => 'TYPO3')
5903
     */
5904
    protected function updateFlexFormData($flexFormId, array $modifications)
5905
    {
5906
        [$table, $uid, $field] = explode(':', $flexFormId, 3);
5907
5908
        if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
5909
            $uid = $this->substNEWwithIDs[$uid];
5910
        }
5911
5912
        $record = $this->recordInfo($table, $uid, '*');
5913
5914
        if (!$table || !$uid || !$field || !is_array($record)) {
5915
            return;
5916
        }
5917
5918
        BackendUtility::workspaceOL($table, $record);
5919
5920
        // Get current data structure and value array:
5921
        $valueStructure = GeneralUtility::xml2array($record[$field]);
5922
5923
        // Do recursive processing of the XML data:
5924
        foreach ($modifications as $path => $value) {
5925
            $valueStructure['data'] = ArrayUtility::setValueByPath(
5926
                $valueStructure['data'],
5927
                $path,
5928
                $value
5929
            );
5930
        }
5931
5932
        if (is_array($valueStructure['data'])) {
5933
            // The return value should be compiled back into XML
5934
            $values = [
5935
                $field => $this->checkValue_flexArray2Xml($valueStructure, true),
5936
            ];
5937
5938
            $this->updateDB($table, $uid, $values);
5939
        }
5940
    }
5941
5942
    /**
5943
     * Triggers a remap action for a specific record.
5944
     *
5945
     * Some records are post-processed by the processRemapStack() method (e.g. IRRE children).
5946
     * This method determines whether an action/modification is executed directly to a record
5947
     * or is postponed to happen after remapping data.
5948
     *
5949
     * @param string $table Name of the table
5950
     * @param string $id Id of the record (can also be a "NEW..." string)
5951
     * @param array $callback The method to be called
5952
     * @param array $arguments The arguments to be submitted to the callback method
5953
     * @param bool $forceRemapStackActions Whether to force to use the stack
5954
     * @see processRemapStack
5955
     */
5956
    protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
5957
    {
5958
        // Check whether the affected record is marked to be remapped:
5959
        if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
5960
            call_user_func_array($callback, $arguments);
5961
        } else {
5962
            $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

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

6801
                    $this->getRecordHistoryStore()->addRecord($table, $id, /** @scrutinizer ignore-type */ $newRow, $this->correlationId);
Loading history...
6802
6803
                    if ($newVersion) {
6804
                        if ($this->enableLogging) {
6805
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6806
                            $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);
6807
                        }
6808
                    } else {
6809
                        if ($this->enableLogging) {
6810
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6811
                            $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
6812
                            $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);
6813
                        }
6814
                        // Clear cache for relevant pages:
6815
                        $this->registerRecordIdForPageCacheClearing($table, $id);
6816
                    }
6817
                    return $id;
6818
                }
6819
                if ($this->enableLogging) {
6820
                    $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

6820
                    $this->log($table, /** @scrutinizer ignore-type */ $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
Loading history...
6821
                }
6822
            }
6823
        }
6824
        return null;
6825
    }
6826
6827
    /**
6828
     * Checking stored record to see if the written values are properly updated.
6829
     *
6830
     * @param string $table Record table name
6831
     * @param int $id Record uid
6832
     * @param array $fieldArray Array of field=>value pairs to insert/update
6833
     * @param string $action Action, for logging only.
6834
     * @return array|null Selected row
6835
     * @see insertDB()
6836
     * @see updateDB()
6837
     * @internal should only be used from within DataHandler
6838
     */
6839
    public function checkStoredRecord($table, $id, $fieldArray, $action)
6840
    {
6841
        $id = (int)$id;
6842
        if (is_array($GLOBALS['TCA'][$table]) && $id) {
6843
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6844
            $queryBuilder->getRestrictions()->removeAll();
6845
6846
            $row = $queryBuilder
6847
                ->select('*')
6848
                ->from($table)
6849
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6850
                ->execute()
6851
                ->fetch();
6852
6853
            if (!empty($row)) {
6854
                // Traverse array of values that was inserted into the database and compare with the actually stored value:
6855
                $errors = [];
6856
                foreach ($fieldArray as $key => $value) {
6857
                    if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
6858
                        if (is_float($row[$key])) {
6859
                            // if the database returns the value as double, compare it as double
6860
                            if ((double)$value !== (double)$row[$key]) {
6861
                                $errors[] = $key;
6862
                            }
6863
                        } else {
6864
                            $dbType = $GLOBALS['TCA'][$table]['columns'][$key]['config']['dbType'] ?? false;
6865
                            if ($dbType === 'datetime' || $dbType === 'time') {
6866
                                $row[$key] = $this->normalizeTimeFormat($table, $row[$key], $dbType);
6867
                            }
6868
                            if ((string)$value !== (string)$row[$key]) {
6869
                                // The is_numeric check catches cases where we want to store a float/double value
6870
                                // and database returns the field as a string with the least required amount of
6871
                                // significant digits, i.e. "0.00" being saved and "0" being read back.
6872
                                if (is_numeric($value) && is_numeric($row[$key])) {
6873
                                    if ((double)$value === (double)$row[$key]) {
6874
                                        continue;
6875
                                    }
6876
                                }
6877
                                $errors[] = $key;
6878
                            }
6879
                        }
6880
                    }
6881
                }
6882
                // Set log message if there were fields with unmatching values:
6883
                if (!empty($errors)) {
6884
                    $message = sprintf(
6885
                        '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.',
6886
                        $id,
6887
                        $table,
6888
                        implode(', ', $errors)
6889
                    );
6890
                    $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

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

7479
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
7480
        return [$parentUid];
7481
    }
7482
7483
    /**
7484
     * Extract entries from TSconfig for a specific table. This will merge specific and default configuration together.
7485
     *
7486
     * @param string $table Table name
7487
     * @param array $TSconfig TSconfig for page
7488
     * @return array TSconfig merged
7489
     * @internal should only be used from within DataHandler
7490
     */
7491
    public function getTableEntries($table, $TSconfig)
7492
    {
7493
        $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : [];
7494
        $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : [];
7495
        ArrayUtility::mergeRecursiveWithOverrule($dA, $tA);
7496
        return $dA;
7497
    }
7498
7499
    /**
7500
     * Returns the pid of a record from $table with $uid
7501
     *
7502
     * @param string $table Table name
7503
     * @param int $uid Record uid
7504
     * @return int|false PID value (unless the record did not exist in which case FALSE is returned)
7505
     * @internal should only be used from within DataHandler
7506
     */
7507
    public function getPID($table, $uid)
7508
    {
7509
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7510
        $queryBuilder->getRestrictions()
7511
            ->removeAll();
7512
        $queryBuilder->select('pid')
7513
            ->from($table)
7514
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
7515
        if ($row = $queryBuilder->execute()->fetch()) {
7516
            return $row['pid'];
7517
        }
7518
        return false;
7519
    }
7520
7521
    /**
7522
     * Executing dbAnalysisStore
7523
     * This will save MM relations for new records but is executed after records are created because we need to know the ID of them
7524
     * @internal should only be used from within DataHandler
7525
     */
7526
    public function dbAnalysisStoreExec()
7527
    {
7528
        foreach ($this->dbAnalysisStore as $action) {
7529
            $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
7530
            if ($id) {
7531
                $action[0]->writeMM($action[1], $id, $action[3]);
7532
            }
7533
        }
7534
    }
7535
7536
    /**
7537
     * Returns array, $CPtable, of pages under the $pid going down to $counter levels.
7538
     * Selecting ONLY pages which the user has read-access to!
7539
     *
7540
     * @param array $CPtable Accumulation of page uid=>pid pairs in branch of $pid
7541
     * @param int $pid Page ID for which to find subpages
7542
     * @param int $counter Number of levels to go down.
7543
     * @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!
7544
     * @return array Return array.
7545
     * @internal should only be used from within DataHandler
7546
     */
7547
    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...
7548
    {
7549
        if ($counter) {
7550
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
7551
            $restrictions = $queryBuilder->getRestrictions()->removeAll();
7552
            $this->addDeleteRestriction($restrictions);
7553
            $queryBuilder
7554
                ->select('uid')
7555
                ->from('pages')
7556
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7557
                ->orderBy('sorting', 'DESC');
7558
            if (!$this->admin) {
7559
                $queryBuilder->andWhere($this->BE_USER->getPagePermsClause(Permission::PAGE_SHOW));
7560
            }
7561
            if ((int)$this->BE_USER->workspace === 0) {
7562
                $queryBuilder->andWhere(
7563
                    $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
7564
                );
7565
            } else {
7566
                $queryBuilder->andWhere($queryBuilder->expr()->in(
7567
                    't3ver_wsid',
7568
                    $queryBuilder->createNamedParameter([0, $this->BE_USER->workspace], Connection::PARAM_INT_ARRAY)
7569
                ));
7570
            }
7571
            $result = $queryBuilder->execute();
7572
7573
            $pages = [];
7574
            while ($row = $result->fetch()) {
7575
                $pages[$row['uid']] = $row;
7576
            }
7577
7578
            // Resolve placeholders of workspace versions
7579
            if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
7580
                $pages = array_reverse(
7581
                    $this->resolveVersionedRecords(
7582
                        'pages',
7583
                        'uid',
7584
                        'sorting',
7585
                        array_keys($pages)
7586
                    ),
7587
                    true
7588
                );
7589
            }
7590
7591
            foreach ($pages as $page) {
7592
                if ($page['uid'] != $rootID) {
7593
                    $CPtable[$page['uid']] = $pid;
7594
                    // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
7595
                    if ($counter - 1) {
7596
                        $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
7597
                    }
7598
                }
7599
            }
7600
        }
7601
        return $CPtable;
7602
    }
7603
7604
    /**
7605
     * List of all tables (those administrators has access to = array_keys of $GLOBALS['TCA'])
7606
     *
7607
     * @return array Array of all TCA table names
7608
     * @internal should only be used from within DataHandler
7609
     */
7610
    public function compileAdminTables()
7611
    {
7612
        return array_keys($GLOBALS['TCA']);
7613
    }
7614
7615
    /**
7616
     * Checks if any uniqueInPid eval input fields are in the record and if so, they are re-written to be correct.
7617
     *
7618
     * @param string $table Table name
7619
     * @param int $uid Record UID
7620
     * @internal should only be used from within DataHandler
7621
     */
7622
    public function fixUniqueInPid($table, $uid)
7623
    {
7624
        if (empty($GLOBALS['TCA'][$table])) {
7625
            return;
7626
        }
7627
7628
        $curData = $this->recordInfo($table, $uid, '*');
7629
        $newData = [];
7630
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7631
            if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
7632
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7633
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
7634
                    $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
7635
                    if ((string)$newV !== (string)$curData[$field]) {
7636
                        $newData[$field] = $newV;
7637
                    }
7638
                }
7639
            }
7640
        }
7641
        // IF there are changed fields, then update the database
7642
        if (!empty($newData)) {
7643
            $this->updateDB($table, $uid, $newData);
7644
        }
7645
    }
7646
7647
    /**
7648
     * Checks if any uniqueInSite eval fields are in the record and if so, they are re-written to be correct.
7649
     *
7650
     * @param string $table Table name
7651
     * @param int $uid Record UID
7652
     * @return bool whether the record had to be fixed or not
7653
     */
7654
    protected function fixUniqueInSite(string $table, int $uid): bool
7655
    {
7656
        $curData = $this->recordInfo($table, $uid, '*');
7657
        $workspaceId = $this->BE_USER->workspace;
7658
        $newData = [];
7659
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7660
            if ($conf['config']['type'] === 'slug' && (string)$curData[$field] !== '') {
7661
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7662
                if (in_array('uniqueInSite', $evalCodesArray, true)) {
7663
                    $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $conf['config'], $workspaceId);
7664
                    $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

7664
                    $state = RecordStateFactory::forName($table)->fromArray(/** @scrutinizer ignore-type */ $curData);
Loading history...
7665
                    $newValue = $helper->buildSlugForUniqueInSite($curData[$field], $state);
7666
                    if ((string)$newValue !== (string)$curData[$field]) {
7667
                        $newData[$field] = $newValue;
7668
                    }
7669
                }
7670
            }
7671
        }
7672
        // IF there are changed fields, then update the database
7673
        if (!empty($newData)) {
7674
            $this->updateDB($table, $uid, $newData);
7675
            return true;
7676
        }
7677
        return false;
7678
    }
7679
7680
    /**
7681
     * Check if there are subpages that need an adoption as well
7682
     * @param int $pageId
7683
     */
7684
    protected function fixUniqueInSiteForSubpages(int $pageId)
7685
    {
7686
        // Get ALL subpages to update - read-permissions are respected
7687
        $subPages = $this->int_pageTreeInfo([], $pageId, 99, $pageId);
7688
        // Now fix uniqueInSite for subpages
7689
        foreach ($subPages as $thePageUid => $thePagePid) {
7690
            $recordWasModified = $this->fixUniqueInSite('pages', $thePageUid);
7691
            if ($recordWasModified) {
7692
                // @todo: Add logging and history - but how? we don't know the data that was in the system before
7693
            }
7694
        }
7695
    }
7696
7697
    /**
7698
     * When er record is copied you can specify fields from the previous record which should be copied into the new one
7699
     * 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)
7700
     *
7701
     * @param string $table Table name
7702
     * @param int $uid Record UID
7703
     * @param int $prevUid UID of previous record
7704
     * @param bool $update If set, updates the record
7705
     * @param array $newData Input array. If fields are already specified AND $update is not set, values are not set in output array.
7706
     * @return array Output array (For when the copying operation needs to get the information instead of updating the info)
7707
     * @internal should only be used from within DataHandler
7708
     */
7709
    public function fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData = [])
7710
    {
7711
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
7712
            $prevData = $this->recordInfo($table, $prevUid, '*');
7713
            $theFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
7714
            foreach ($theFields as $field) {
7715
                if ($GLOBALS['TCA'][$table]['columns'][$field] && ($update || !isset($newData[$field]))) {
7716
                    $newData[$field] = $prevData[$field];
7717
                }
7718
            }
7719
            if ($update && !empty($newData)) {
7720
                $this->updateDB($table, $uid, $newData);
7721
            }
7722
        }
7723
        return $newData;
7724
    }
7725
7726
    /**
7727
     * Casts a reference value. In case MM relations or foreign_field
7728
     * references are used. All other configurations, as well as
7729
     * foreign_table(!) could be stored as comma-separated-values
7730
     * as well. Since the system is not able to determine the default
7731
     * value automatically then, the TCA default value is used if
7732
     * it has been defined.
7733
     *
7734
     * @param int|string $value The value to be casted (e.g. '', '0', '1,2,3')
7735
     * @param array $configuration The TCA configuration of the accordant field
7736
     * @return int|string
7737
     */
7738
    protected function castReferenceValue($value, array $configuration)
7739
    {
7740
        if ((string)$value !== '') {
7741
            return $value;
7742
        }
7743
7744
        if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
7745
            return 0;
7746
        }
7747
7748
        if (array_key_exists('default', $configuration)) {
7749
            return $configuration['default'];
7750
        }
7751
7752
        return $value;
7753
    }
7754
7755
    /**
7756
     * Returns TRUE if the TCA/columns field type is a DB reference field
7757
     *
7758
     * @param array $conf Config array for TCA/columns field
7759
     * @return bool TRUE if DB reference field (group/db or select with foreign-table)
7760
     * @internal should only be used from within DataHandler
7761
     */
7762
    public function isReferenceField($conf)
7763
    {
7764
        return $conf['type'] === 'group' && $conf['internal_type'] === 'db' || $conf['type'] === 'select' && $conf['foreign_table'];
7765
    }
7766
7767
    /**
7768
     * Returns the subtype as a string of an inline field.
7769
     * If it's not an inline field at all, it returns FALSE.
7770
     *
7771
     * @param array $conf Config array for TCA/columns field
7772
     * @return string|bool string Inline subtype (field|mm|list), boolean: FALSE
7773
     * @internal should only be used from within DataHandler
7774
     */
7775
    public function getInlineFieldType($conf)
7776
    {
7777
        if ($conf['type'] !== 'inline' || !$conf['foreign_table']) {
7778
            return false;
7779
        }
7780
        if ($conf['foreign_field']) {
7781
            // The reference to the parent is stored in a pointer field in the child record
7782
            return 'field';
7783
        }
7784
        if ($conf['MM']) {
7785
            // Regular MM intermediate table is used to store data
7786
            return 'mm';
7787
        }
7788
        // An item list (separated by comma) is stored (like select type is doing)
7789
        return 'list';
7790
    }
7791
7792
    /**
7793
     * Get modified header for a copied record
7794
     *
7795
     * @param string $table Table name
7796
     * @param int $pid PID value in which other records to test might be
7797
     * @param string $field Field name to get header value for.
7798
     * @param string $value Current field value
7799
     * @param int $count Counter (number of recursions)
7800
     * @param string $prevTitle Previous title we checked for (in previous recursion)
7801
     * @return string The field value, possibly appended with a "copy label
7802
     * @internal should only be used from within DataHandler
7803
     */
7804
    public function getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
7805
    {
7806
        // Set title value to check for:
7807
        $checkTitle = $value;
7808
        if ($count > 0) {
7809
            $checkTitle = $value . rtrim(' ' . sprintf($this->prependLabel($table), $count));
7810
        }
7811
        // Do check:
7812
        if ($prevTitle != $checkTitle || $count < 100) {
7813
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7814
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7815
            $rowCount = $queryBuilder
7816
                ->count('uid')
7817
                ->from($table)
7818
                ->where(
7819
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
7820
                    $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($checkTitle, \PDO::PARAM_STR))
7821
                )
7822
                ->execute()
7823
                ->fetchColumn(0);
7824
            if ($rowCount) {
7825
                return $this->getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
7826
            }
7827
        }
7828
        // Default is to just return the current input title if no other was returned before:
7829
        return $checkTitle;
7830
    }
7831
7832
    /**
7833
     * Return "copy" label for a table. Although the name is "prepend" it actually APPENDs the label (after ...)
7834
     *
7835
     * @param string $table Table name
7836
     * @return string Label to append, containing "%s" for the number
7837
     * @see getCopyHeader()
7838
     * @internal should only be used from within DataHandler
7839
     */
7840
    public function prependLabel($table)
7841
    {
7842
        return $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
7843
    }
7844
7845
    /**
7846
     * Get the final pid based on $table and $pid ($destPid type... pos/neg)
7847
     *
7848
     * @param string $table Table name
7849
     * @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!
7850
     * @return int
7851
     * @internal should only be used from within DataHandler
7852
     */
7853
    public function resolvePid($table, $pid)
7854
    {
7855
        $pid = (int)$pid;
7856
        if ($pid < 0) {
7857
            $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7858
            $query->getRestrictions()
7859
                ->removeAll();
7860
            $row = $query
7861
                ->select('pid')
7862
                ->from($table)
7863
                ->where($query->expr()->eq('uid', $query->createNamedParameter(abs($pid), \PDO::PARAM_INT)))
7864
                ->execute()
7865
                ->fetch();
7866
            $pid = (int)$row['pid'];
7867
        }
7868
        return $pid;
7869
    }
7870
7871
    /**
7872
     * Removes the prependAtCopy prefix on values
7873
     *
7874
     * @param string $table Table name
7875
     * @param string $value The value to fix
7876
     * @return string Clean name
7877
     * @internal should only be used from within DataHandler
7878
     */
7879
    public function clearPrefixFromValue($table, $value)
7880
    {
7881
        $regex = '/\s' . sprintf(preg_quote($this->prependLabel($table)), '[0-9]*') . '$/';
7882
        return @preg_replace($regex, '', $value);
7883
    }
7884
7885
    /**
7886
     * Check if there are records from tables on the pages to be deleted which the current user is not allowed to
7887
     *
7888
     * @param int[] $pageIds IDs of pages which should be checked
7889
     * @return bool Return TRUE, if permission granted
7890
     * @see canDeletePage()
7891
     */
7892
    protected function checkForRecordsFromDisallowedTables(array $pageIds)
7893
    {
7894
        if ($this->admin) {
7895
            return true;
7896
        }
7897
7898
        if (!empty($pageIds)) {
7899
            $tableNames = $this->compileAdminTables();
7900
            foreach ($tableNames as $table) {
7901
                $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7902
                $query->getRestrictions()
7903
                    ->removeAll()
7904
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7905
                $count = $query->count('uid')
7906
                    ->from($table)
7907
                    ->where($query->expr()->in(
7908
                        'pid',
7909
                        $query->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
7910
                    ))
7911
                    ->execute()
7912
                    ->fetchColumn(0);
7913
                if ($count && ($this->tableReadOnly($table) || !$this->checkModifyAccessList($table))) {
7914
                    return false;
7915
                }
7916
            }
7917
        }
7918
        return true;
7919
    }
7920
7921
    /**
7922
     * Determine if a record was copied or if a record is the result of a copy action.
7923
     *
7924
     * @param string $table The tablename of the record
7925
     * @param int $uid The uid of the record
7926
     * @return bool Returns TRUE if the record is copied or is the result of a copy action
7927
     * @internal should only be used from within DataHandler
7928
     */
7929
    public function isRecordCopied($table, $uid)
7930
    {
7931
        // If the record was copied:
7932
        if (isset($this->copyMappingArray[$table][$uid])) {
7933
            return true;
7934
        }
7935
        if (isset($this->copyMappingArray[$table]) && in_array($uid, array_values($this->copyMappingArray[$table]))) {
7936
            return true;
7937
        }
7938
        return false;
7939
    }
7940
7941
    /******************************
7942
     *
7943
     * Clearing cache
7944
     *
7945
     ******************************/
7946
7947
    /**
7948
     * Clearing the cache based on a page being updated
7949
     * If the $table is 'pages' then cache is cleared for all pages on the same level (and subsequent?)
7950
     * Else just clear the cache for the parent page of the record.
7951
     *
7952
     * @param string $table Table name of record that was just updated.
7953
     * @param int $uid UID of updated / inserted record
7954
     * @param int $pid REAL PID of page of a deleted/moved record to get TSconfig in ClearCache.
7955
     * @internal This method is not meant to be called directly but only from the core itself or from hooks
7956
     */
7957
    public function registerRecordIdForPageCacheClearing($table, $uid, $pid = null)
7958
    {
7959
        if (!is_array(static::$recordsToClearCacheFor[$table])) {
7960
            static::$recordsToClearCacheFor[$table] = [];
7961
        }
7962
        static::$recordsToClearCacheFor[$table][] = (int)$uid;
7963
        if ($pid !== null) {
7964
            if (!is_array(static::$recordPidsForDeletedRecords[$table])) {
7965
                static::$recordPidsForDeletedRecords[$table] = [];
7966
            }
7967
            static::$recordPidsForDeletedRecords[$table][$uid][] = (int)$pid;
7968
        }
7969
    }
7970
7971
    /**
7972
     * Do the actual clear cache
7973
     */
7974
    protected function processClearCacheQueue()
7975
    {
7976
        $tagsToClear = [];
7977
        $clearCacheCommands = [];
7978
7979
        foreach (static::$recordsToClearCacheFor as $table => $uids) {
7980
            foreach (array_unique($uids) as $uid) {
7981
                if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
7982
                    return;
7983
                }
7984
                // For move commands we may get more then 1 parent.
7985
                $pageUids = $this->getOriginalParentOfRecord($table, $uid);
7986
                foreach ($pageUids as $originalParent) {
7987
                    [$tagsToClearFromPrepare, $clearCacheCommandsFromPrepare]
7988
                        = $this->prepareCacheFlush($table, $uid, $originalParent);
7989
                    $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
7990
                    $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
7991
                }
7992
            }
7993
        }
7994
7995
        /** @var CacheManager $cacheManager */
7996
        $cacheManager = $this->getCacheManager();
7997
        $cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear));
7998
7999
        // Filter duplicate cache commands from cacheQueue
8000
        $clearCacheCommands = array_unique($clearCacheCommands);
8001
        // Execute collected clear cache commands from page TSConfig
8002
        foreach ($clearCacheCommands as $command) {
8003
            $this->clear_cacheCmd($command);
8004
        }
8005
8006
        // Reset the cache clearing array
8007
        static::$recordsToClearCacheFor = [];
8008
8009
        // Reset the original pid array
8010
        static::$recordPidsForDeletedRecords = [];
8011
    }
8012
8013
    /**
8014
     * Prepare the cache clearing
8015
     *
8016
     * @param string $table Table name of record that needs to be cleared
8017
     * @param int $uid UID of record for which the cache needs to be cleared
8018
     * @param int $pid Original pid of the page of the record which the cache needs to be cleared
8019
     * @return array Array with tagsToClear and clearCacheCommands
8020
     * @internal This function is internal only it may be changed/removed also in minor version numbers.
8021
     */
8022
    protected function prepareCacheFlush($table, $uid, $pid)
8023
    {
8024
        $tagsToClear = [];
8025
        $clearCacheCommands = [];
8026
        $pageUid = 0;
8027
        // Get Page TSconfig relevant:
8028
        $TSConfig = BackendUtility::getPagesTSconfig($pid)['TCEMAIN.'] ?? [];
8029
        if (empty($TSConfig['clearCache_disable']) && $this->BE_USER->workspace === 0) {
8030
            $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
8031
            // If table is "pages":
8032
            $pageIdsThatNeedCacheFlush = [];
8033
            if ($table === 'pages') {
8034
                // Find out if the record is a get the original page
8035
                $pageUid = $this->getDefaultLanguagePageId($uid);
8036
8037
                // Builds list of pages on the SAME level as this page (siblings)
8038
                $queryBuilder = $connectionPool->getQueryBuilderForTable('pages');
8039
                $queryBuilder->getRestrictions()
8040
                    ->removeAll()
8041
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8042
                $siblings = $queryBuilder
8043
                    ->select('A.pid AS pid', 'B.uid AS uid')
8044
                    ->from('pages', 'A')
8045
                    ->from('pages', 'B')
8046
                    ->where(
8047
                        $queryBuilder->expr()->eq('A.uid', $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)),
8048
                        $queryBuilder->expr()->eq('B.pid', $queryBuilder->quoteIdentifier('A.pid')),
8049
                        $queryBuilder->expr()->gte('A.pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8050
                    )
8051
                    ->execute();
8052
8053
                $parentPageId = 0;
8054
                while ($row_tmp = $siblings->fetch()) {
8055
                    $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
8056
                    $parentPageId = (int)$row_tmp['pid'];
8057
                    // Add children as well:
8058
                    if ($TSConfig['clearCache_pageSiblingChildren']) {
8059
                        $siblingChildrenQuery = $connectionPool->getQueryBuilderForTable('pages');
8060
                        $siblingChildrenQuery->getRestrictions()
8061
                            ->removeAll()
8062
                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8063
                        $siblingChildren = $siblingChildrenQuery
8064
                            ->select('uid')
8065
                            ->from('pages')
8066
                            ->where($siblingChildrenQuery->expr()->eq(
8067
                                'pid',
8068
                                $siblingChildrenQuery->createNamedParameter($row_tmp['uid'], \PDO::PARAM_INT)
8069
                            ))
8070
                            ->execute();
8071
                        while ($row_tmp2 = $siblingChildren->fetch()) {
8072
                            $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
8073
                        }
8074
                    }
8075
                }
8076
                // Finally, add the parent page as well when clearing a specific page
8077
                if ($parentPageId > 0) {
8078
                    $pageIdsThatNeedCacheFlush[] = $parentPageId;
8079
                }
8080
                // Add grand-parent as well if configured
8081
                if ($TSConfig['clearCache_pageGrandParent']) {
8082
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8083
                    $parentQuery->getRestrictions()
8084
                        ->removeAll()
8085
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8086
                    $row_tmp = $parentQuery
8087
                        ->select('pid')
8088
                        ->from('pages')
8089
                        ->where($parentQuery->expr()->eq(
8090
                            'uid',
8091
                            $parentQuery->createNamedParameter($parentPageId, \PDO::PARAM_INT)
8092
                        ))
8093
                        ->execute()
8094
                        ->fetch();
8095
                    if (!empty($row_tmp)) {
8096
                        $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
8097
                    }
8098
                }
8099
            } else {
8100
                // For other tables than "pages", delete cache for the records "parent page".
8101
                $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
8102
                // Add the parent page as well
8103
                if ($TSConfig['clearCache_pageGrandParent']) {
8104
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8105
                    $parentQuery->getRestrictions()
8106
                        ->removeAll()
8107
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8108
                    $parentPageRecord = $parentQuery
8109
                        ->select('pid')
8110
                        ->from('pages')
8111
                        ->where($parentQuery->expr()->eq(
8112
                            'uid',
8113
                            $parentQuery->createNamedParameter($pageUid, \PDO::PARAM_INT)
8114
                        ))
8115
                        ->execute()
8116
                        ->fetch();
8117
                    if (!empty($parentPageRecord)) {
8118
                        $pageIdsThatNeedCacheFlush[] = (int)$parentPageRecord['pid'];
8119
                    }
8120
                }
8121
            }
8122
            // Call pre-processing function for clearing of cache for page ids:
8123
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8124
                $_params = ['pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()'];
8125
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8126
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8127
            }
8128
            // Delete cache for selected pages:
8129
            foreach ($pageIdsThatNeedCacheFlush as $pageId) {
8130
                $tagsToClear['pageId_' . $pageId] = true;
8131
            }
8132
            // Queue delete cache for current table and record
8133
            $tagsToClear[$table] = true;
8134
            $tagsToClear[$table . '_' . $uid] = true;
8135
        }
8136
        // Clear cache for pages entered in TSconfig:
8137
        if (!empty($TSConfig['clearCacheCmd'])) {
8138
            $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
8139
            $clearCacheCommands = array_unique($commands);
8140
        }
8141
        // Call post processing function for clear-cache:
8142
        $_params = ['table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig, 'tags' => $tagsToClear];
8143
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8144
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8145
        }
8146
        return [
8147
            $tagsToClear,
8148
            $clearCacheCommands
8149
        ];
8150
    }
8151
8152
    /**
8153
     * Clears the cache based on the command $cacheCmd.
8154
     *
8155
     * $cacheCmd='pages'
8156
     * Clears cache for all pages and page-based caches inside the cache manager.
8157
     * Requires admin-flag to be set for BE_USER.
8158
     *
8159
     * $cacheCmd='all'
8160
     * Clears all cache_tables. This is necessary if templates are updated.
8161
     * Requires admin-flag to be set for BE_USER.
8162
     *
8163
     * The following cache_* are intentionally not cleared by 'all'
8164
     *
8165
     * - imagesizes:	Clearing this table would cause a lot of unneeded
8166
     * Imagemagick calls because the size information has
8167
     * to be fetched again after clearing.
8168
     * - all caches inside the cache manager that are inside the group "system"
8169
     * - they are only needed to build up the core system and templates.
8170
     *   If the group of system caches needs to be deleted explicitly, use
8171
     *   flushCachesInGroup('system') of CacheManager directly.
8172
     *
8173
     * $cacheCmd=[integer]
8174
     * Clears cache for the page pointed to by $cacheCmd (an integer).
8175
     *
8176
     * $cacheCmd='cacheTag:[string]'
8177
     * Flush page and pagesection cache by given tag
8178
     *
8179
     * $cacheCmd='cacheId:[string]'
8180
     * Removes cache identifier from page and page section cache
8181
     *
8182
     * Can call a list of post processing functions as defined in
8183
     * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc']
8184
     * (numeric array with values being the function references, called by
8185
     * GeneralUtility::callUserFunction()).
8186
     *
8187
     *
8188
     * @param string $cacheCmd The cache command, see above description
8189
     */
8190
    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...
8191
    {
8192
        if (is_object($this->BE_USER)) {
8193
            $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]);
8194
        }
8195
        $userTsConfig = $this->BE_USER->getTSConfig();
8196
        switch (strtolower($cacheCmd)) {
8197
            case 'pages':
8198
                if ($this->admin || ($userTsConfig['options.']['clearCache.']['pages'] ?? false)) {
8199
                    $this->getCacheManager()->flushCachesInGroup('pages');
8200
                }
8201
                break;
8202
            case 'all':
8203
                // allow to clear all caches if the TS config option is enabled or the option is not explicitly
8204
                // disabled for admins (which could clear all caches by default). The latter option is useful
8205
                // for big production sites where it should be possible to restrict the cache clearing for some admins.
8206
                if (($userTsConfig['options.']['clearCache.']['all'] ?? false)
8207
                    || ($this->admin && (bool)($userTsConfig['options.']['clearCache.']['all'] ?? true))
8208
                ) {
8209
                    $this->getCacheManager()->flushCaches();
8210
                    GeneralUtility::makeInstance(ConnectionPool::class)
8211
                        ->getConnectionForTable('cache_treelist')
8212
                        ->truncate('cache_treelist');
8213
8214
                    // Delete Opcode Cache
8215
                    GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
8216
                }
8217
                break;
8218
        }
8219
8220
        $tagsToFlush = [];
8221
        // Clear cache for a page ID!
8222
        if (MathUtility::canBeInterpretedAsInteger($cacheCmd)) {
8223
            $list_cache = [$cacheCmd];
8224
            // Call pre-processing function for clearing of cache for page ids:
8225
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8226
                $_params = ['pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()'];
8227
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8228
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8229
            }
8230
            // Delete cache for selected pages:
8231
            if (is_array($list_cache)) {
0 ignored issues
show
introduced by
The condition is_array($list_cache) is always true.
Loading history...
8232
                foreach ($list_cache as $pageId) {
8233
                    $tagsToFlush[] = 'pageId_' . (int)$pageId;
8234
                }
8235
            }
8236
        }
8237
        // flush cache by tag
8238
        if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
8239
            $cacheTag = substr($cacheCmd, 9);
8240
            $tagsToFlush[] = $cacheTag;
8241
        }
8242
        // process caching framework operations
8243
        if (!empty($tagsToFlush)) {
8244
            $this->getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);
0 ignored issues
show
Bug introduced by
The method getCacheManager() does not exist on null. ( Ignorable by Annotation )

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

8244
            $this->/** @scrutinizer ignore-call */ 
8245
                   getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
8245
        }
8246
8247
        // Call post processing function for clear-cache:
8248
        $_params = ['cacheCmd' => strtolower($cacheCmd), 'tags' => $tagsToFlush];
8249
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8250
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8251
        }
8252
    }
8253
8254
    /*****************************
8255
     *
8256
     * Logging
8257
     *
8258
     *****************************/
8259
    /**
8260
     * Logging actions from DataHandler
8261
     *
8262
     * @param string $table Table name the log entry is concerned with. Blank if NA
8263
     * @param int $recuid Record UID. Zero if NA
8264
     * @param int $action Action number: 0=No category, 1=new record, 2=update record, 3= delete record, 4= move record, 5= Check/evaluate
8265
     * @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
8266
     * @param int $error The severity: 0 = message, 1 = error, 2 = System Error, 3 = security notice (admin), 4 warning
8267
     * @param string $details Default error message in english
8268
     * @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
8269
     * @param array $data Array with special information that may go into $details by '%s' marks / sprintf() when the log is shown
8270
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
8271
     * @param string $NEWid NEW id for new records
8272
     * @return int Log entry UID (0 if no log entry was written or logging is disabled)
8273
     * @see \TYPO3\CMS\Core\SysLog\Action\Database for all available values of argument $action
8274
     * @see \TYPO3\CMS\Core\SysLog\Error for all available values of argument $error
8275
     * @internal should only be used from within TYPO3 Core
8276
     */
8277
    public function log($table, $recuid, $action, $recpid, $error, $details, $details_nr = -1, $data = [], $event_pid = -1, $NEWid = '')
8278
    {
8279
        if (!$this->enableLogging) {
8280
            return 0;
8281
        }
8282
        // Type value for DataHandler
8283
        if (!$this->storeLogMessages) {
8284
            $details = '';
8285
        }
8286
        if ($error > 0) {
8287
            $detailMessage = $details;
8288
            if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
8289
                $detailMessage = vsprintf($details, $data);
8290
            }
8291
            $this->errorLog[] = '[' . SystemLogType::DB . '.' . $action . '.' . $details_nr . ']: ' . $detailMessage;
8292
        }
8293
        return $this->BE_USER->writelog(SystemLogType::DB, $action, $error, $details_nr, $details, $data, $table, $recuid, $recpid, $event_pid, $NEWid);
8294
    }
8295
8296
    /**
8297
     * Simple logging function meant to be used when logging messages is not yet fixed.
8298
     *
8299
     * @param string $message Message string
8300
     * @param int $error Error code, see log()
8301
     * @return int Log entry UID
8302
     * @see log()
8303
     * @internal should only be used from within TYPO3 Core
8304
     */
8305
    public function newlog($message, $error = SystemLogErrorClassification::MESSAGE)
8306
    {
8307
        return $this->log('', 0, SystemLogGenericAction::UNDEFINED, 0, $error, $message, -1);
8308
    }
8309
8310
    /**
8311
     * Print log error messages from the operations of this script instance
8312
     * @internal should only be used from within TYPO3 Core
8313
     */
8314
    public function printLogErrorMessages()
8315
    {
8316
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
8317
        $queryBuilder->getRestrictions()->removeAll();
8318
        $result = $queryBuilder
8319
            ->select('*')
8320
            ->from('sys_log')
8321
            ->where(
8322
                $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
8323
                $queryBuilder->expr()->lt('action', $queryBuilder->createNamedParameter(256, \PDO::PARAM_INT)),
8324
                $queryBuilder->expr()->eq(
8325
                    'userid',
8326
                    $queryBuilder->createNamedParameter($this->BE_USER->user['uid'], \PDO::PARAM_INT)
8327
                ),
8328
                $queryBuilder->expr()->eq(
8329
                    'tstamp',
8330
                    $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
8331
                ),
8332
                $queryBuilder->expr()->neq('error', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8333
            )
8334
            ->execute();
8335
8336
        while ($row = $result->fetch()) {
8337
            $log_data = unserialize($row['log_data']);
8338
            $msg = $row['error'] . ': ' . sprintf($row['details'], $log_data[0], $log_data[1], $log_data[2], $log_data[3], $log_data[4]);
8339
            /** @var FlashMessage $flashMessage */
8340
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', $row['error'] === 4 ? FlashMessage::WARNING : FlashMessage::ERROR, true);
8341
            /** @var FlashMessageService $flashMessageService */
8342
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
8343
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
8344
            $defaultFlashMessageQueue->enqueue($flashMessage);
8345
        }
8346
    }
8347
8348
    /*****************************
8349
     *
8350
     * Internal (do not use outside Core!)
8351
     *
8352
     *****************************/
8353
8354
    /**
8355
     * Find out if the record is a get the original page
8356
     *
8357
     * @param int $pageId the page UID (can be the default page record, or a page translation record ID)
8358
     * @return int the page UID of the default page record
8359
     */
8360
    protected function getDefaultLanguagePageId(int $pageId): int
8361
    {
8362
        $localizationParentFieldName = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
8363
        $row = $this->recordInfo('pages', $pageId, $localizationParentFieldName);
8364
        $localizationParent = (int)$row[$localizationParentFieldName];
8365
        if ($localizationParent > 0) {
8366
            return $localizationParent;
8367
        }
8368
        return $pageId;
8369
    }
8370
8371
    /**
8372
     * Preprocesses field array based on field type. Some fields must be adjusted
8373
     * before going to database. This is done on the copy of the field array because
8374
     * original values are used in remap action later.
8375
     *
8376
     * @param string $table	Table name
8377
     * @param array $fieldArray	Field array to check
8378
     * @return array Updated field array
8379
     * @internal should only be used from within TYPO3 Core
8380
     */
8381
    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...
8382
    {
8383
        $result = $fieldArray;
8384
        foreach ($fieldArray as $field => $value) {
8385
            if (!MathUtility::canBeInterpretedAsInteger($value)
8386
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'inline'
8387
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_field']) {
8388
                $result[$field] = count(GeneralUtility::trimExplode(',', $value, true));
8389
            }
8390
        }
8391
        return $result;
8392
    }
8393
8394
    /**
8395
     * Determines whether a particular record has been deleted
8396
     * using DataHandler::deleteRecord() in this instance.
8397
     *
8398
     * @param string $tableName
8399
     * @param string $uid
8400
     * @return bool
8401
     * @internal should only be used from within TYPO3 Core
8402
     */
8403
    public function hasDeletedRecord($tableName, $uid)
8404
    {
8405
        return
8406
            !empty($this->deletedRecords[$tableName])
8407
            && in_array($uid, $this->deletedRecords[$tableName])
8408
        ;
0 ignored issues
show
Coding Style introduced by
Space found before semicolon; expected ");" but found ")
;"
Loading history...
8409
    }
8410
8411
    /**
8412
     * Gets the automatically versionized id of a record.
8413
     *
8414
     * @param string $table Name of the table
8415
     * @param int $id Uid of the record
8416
     * @return int|null
8417
     * @internal should only be used from within TYPO3 Core
8418
     */
8419
    public function getAutoVersionId($table, $id): ?int
8420
    {
8421
        $result = null;
8422
        if (isset($this->autoVersionIdMap[$table][$id])) {
8423
            $result = (int)trim($this->autoVersionIdMap[$table][$id]);
8424
        }
8425
        return $result;
8426
    }
8427
8428
    /**
8429
     * Overlays the automatically versionized id of a record.
8430
     *
8431
     * @param string $table Name of the table
8432
     * @param int $id Uid of the record
8433
     * @return int
8434
     */
8435
    protected function overlayAutoVersionId($table, $id)
8436
    {
8437
        $autoVersionId = $this->getAutoVersionId($table, $id);
8438
        if ($autoVersionId !== null) {
8439
            $id = $autoVersionId;
8440
        }
8441
        return $id;
8442
    }
8443
8444
    /**
8445
     * Adds new values to the remapStackChildIds array.
8446
     *
8447
     * @param array $idValues uid values
8448
     */
8449
    protected function addNewValuesToRemapStackChildIds(array $idValues)
8450
    {
8451
        foreach ($idValues as $idValue) {
8452
            if (strpos($idValue, 'NEW') === 0) {
8453
                $this->remapStackChildIds[$idValue] = true;
8454
            }
8455
        }
8456
    }
8457
8458
    /**
8459
     * Resolves versioned records for the current workspace scope.
8460
     * Delete placeholders and move placeholders are substituted and removed.
8461
     *
8462
     * @param string $tableName Name of the table to be processed
8463
     * @param string $fieldNames List of the field names to be fetched
8464
     * @param string $sortingField Name of the sorting field to be used
8465
     * @param array $liveIds Flat array of (live) record ids
8466
     * @return array
8467
     */
8468
    protected function resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
8469
    {
8470
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
8471
            ->getConnectionForTable($tableName);
8472
        $sortingStatement = !empty($sortingField)
8473
            ? [$connection->quoteIdentifier($sortingField)]
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
8474
            : null;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
8475
        /** @var PlainDataResolver $resolver */
8476
        $resolver = GeneralUtility::makeInstance(
8477
            PlainDataResolver::class,
8478
            $tableName,
8479
            $liveIds,
8480
            $sortingStatement
8481
        );
8482
8483
        $resolver->setWorkspaceId($this->BE_USER->workspace);
8484
        $resolver->setKeepDeletePlaceholder(false);
8485
        $resolver->setKeepMovePlaceholder(false);
8486
        $resolver->setKeepLiveIds(true);
8487
        $recordIds = $resolver->get();
8488
8489
        $records = [];
8490
        foreach ($recordIds as $recordId) {
8491
            $records[$recordId] = BackendUtility::getRecord($tableName, $recordId, $fieldNames);
8492
        }
8493
8494
        return $records;
8495
    }
8496
8497
    /**
8498
     * Gets the outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler
8499
     * Since \TYPO3\CMS\Core\DataHandling\DataHandler can create nested objects of itself,
8500
     * this method helps to determine the first (= outer most) one.
8501
     *
8502
     * @return DataHandler
8503
     */
8504
    protected function getOuterMostInstance()
8505
    {
8506
        if (!isset($this->outerMostInstance)) {
8507
            $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
8508
            foreach ($stack as $stackItem) {
8509
                if (isset($stackItem['object']) && $stackItem['object'] instanceof self) {
8510
                    $this->outerMostInstance = $stackItem['object'];
8511
                    break;
8512
                }
8513
            }
8514
        }
8515
        return $this->outerMostInstance;
8516
    }
8517
8518
    /**
8519
     * Determines whether the this object is the outer most instance of itself
8520
     * Since DataHandler can create nested objects of itself,
8521
     * this method helps to determine the first (= outer most) one.
8522
     *
8523
     * @return bool
8524
     */
8525
    public function isOuterMostInstance()
8526
    {
8527
        return $this->getOuterMostInstance() === $this;
8528
    }
8529
8530
    /**
8531
     * Gets an instance of the runtime cache.
8532
     *
8533
     * @return FrontendInterface
8534
     */
8535
    protected function getRuntimeCache()
8536
    {
8537
        return $this->getCacheManager()->getCache('runtime');
8538
    }
8539
8540
    /**
8541
     * Determines nested element calls.
8542
     *
8543
     * @param string $table Name of the table
8544
     * @param int $id Uid of the record
8545
     * @param string $identifier Name of the action to be checked
8546
     * @return bool
8547
     */
8548
    protected function isNestedElementCallRegistered($table, $id, $identifier)
8549
    {
8550
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8551
        return isset($nestedElementCalls[$identifier][$table][$id]);
8552
    }
8553
8554
    /**
8555
     * Registers nested elements calls.
8556
     * This is used to track nested calls (e.g. for following m:n relations).
8557
     *
8558
     * @param string $table Name of the table
8559
     * @param int $id Uid of the record
8560
     * @param string $identifier Name of the action to be tracked
8561
     */
8562
    protected function registerNestedElementCall($table, $id, $identifier)
8563
    {
8564
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8565
        $nestedElementCalls[$identifier][$table][$id] = true;
8566
        $this->runtimeCache->set($this->cachePrefixNestedElementCalls, $nestedElementCalls);
8567
    }
8568
8569
    /**
8570
     * Resets the nested element calls.
8571
     */
8572
    protected function resetNestedElementCalls()
8573
    {
8574
        $this->runtimeCache->remove($this->cachePrefixNestedElementCalls);
8575
    }
8576
8577
    /**
8578
     * Determines whether an element was registered to be deleted in the registry.
8579
     *
8580
     * @param string $table Name of the table
8581
     * @param int $id Uid of the record
8582
     * @return bool
8583
     * @see registerElementsToBeDeleted
8584
     * @see resetElementsToBeDeleted
8585
     * @see copyRecord_raw
8586
     * @see versionizeRecord
8587
     */
8588
    protected function isElementToBeDeleted($table, $id)
8589
    {
8590
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8591
        return isset($elementsToBeDeleted[$table][$id]);
8592
    }
8593
8594
    /**
8595
     * Registers elements to be deleted in the registry.
8596
     *
8597
     * @see process_datamap
8598
     */
8599
    protected function registerElementsToBeDeleted()
8600
    {
8601
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8602
        $this->runtimeCache->set('core-datahandler-elementsToBeDeleted', array_merge($elementsToBeDeleted, $this->getCommandMapElements('delete')));
8603
    }
8604
8605
    /**
8606
     * Resets the elements to be deleted in the registry.
8607
     *
8608
     * @see process_datamap
8609
     */
8610
    protected function resetElementsToBeDeleted()
8611
    {
8612
        $this->runtimeCache->remove('core-datahandler-elementsToBeDeleted');
8613
    }
8614
8615
    /**
8616
     * Unsets elements (e.g. of the data map) that shall be deleted.
8617
     * This avoids to modify records that will be deleted later on.
8618
     *
8619
     * @param array $elements Elements to be modified
8620
     * @return array
8621
     */
8622
    protected function unsetElementsToBeDeleted(array $elements)
8623
    {
8624
        $elements = ArrayUtility::arrayDiffAssocRecursive($elements, $this->getCommandMapElements('delete'));
8625
        foreach ($elements as $key => $value) {
8626
            if (empty($value)) {
8627
                unset($elements[$key]);
8628
            }
8629
        }
8630
        return $elements;
8631
    }
8632
8633
    /**
8634
     * Gets elements of the command map that match a particular command.
8635
     *
8636
     * @param string $needle The command to be matched
8637
     * @return array
8638
     */
8639
    protected function getCommandMapElements($needle)
8640
    {
8641
        $elements = [];
8642
        foreach ($this->cmdmap as $tableName => $idArray) {
8643
            foreach ($idArray as $id => $commandArray) {
8644
                foreach ($commandArray as $command => $value) {
8645
                    if ($value && $command == $needle) {
8646
                        $elements[$tableName][$id] = true;
8647
                    }
8648
                }
8649
            }
8650
        }
8651
        return $elements;
8652
    }
8653
8654
    /**
8655
     * Controls active elements and sets NULL values if not active.
8656
     * Datamap is modified accordant to submitted control values.
8657
     */
8658
    protected function controlActiveElements()
8659
    {
8660
        if (!empty($this->control['active'])) {
8661
            $this->setNullValues(
8662
                $this->control['active'],
8663
                $this->datamap
8664
            );
8665
        }
8666
    }
8667
8668
    /**
8669
     * Sets NULL values in haystack array.
8670
     * The general behaviour in the user interface is to enable/activate fields.
8671
     * Thus, this method uses NULL as value to be stored if a field is not active.
8672
     *
8673
     * @param array $active hierarchical array with active elements
8674
     * @param array $haystack hierarchical array with haystack to be modified
8675
     */
8676
    protected function setNullValues(array $active, array &$haystack)
8677
    {
8678
        foreach ($active as $key => $value) {
8679
            // Nested data is processes recursively
8680
            if (is_array($value)) {
8681
                $this->setNullValues(
8682
                    $value,
8683
                    $haystack[$key]
8684
                );
8685
            } elseif ($value == 0) {
8686
                // Field has not been activated in the user interface,
8687
                // thus a NULL value shall be stored in the database
8688
                $haystack[$key] = null;
8689
            }
8690
        }
8691
    }
8692
8693
    /**
8694
     * @param CorrelationId $correlationId
8695
     */
8696
    public function setCorrelationId(CorrelationId $correlationId): void
8697
    {
8698
        $this->correlationId = $correlationId;
8699
    }
8700
8701
    /**
8702
     * @return CorrelationId|null
8703
     */
8704
    public function getCorrelationId(): ?CorrelationId
8705
    {
8706
        return $this->correlationId;
8707
    }
8708
8709
    /**
8710
     * Entry point to post process a database insert. Currently bails early unless a UID has been forced
8711
     * and the database platform is not MySQL.
8712
     *
8713
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8714
     * @param string $tableName
8715
     * @param int $suggestedUid
8716
     * @return int
8717
     */
8718
    protected function postProcessDatabaseInsert(Connection $connection, string $tableName, int $suggestedUid): int
8719
    {
8720
        if ($suggestedUid !== 0 && $connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
8721
            $this->postProcessPostgresqlInsert($connection, $tableName);
8722
            // The last inserted id on postgresql is actually the last value generated by the sequence.
8723
            // On a forced UID insert this might not be the actual value or the sequence might not even
8724
            // have generated a value yet.
8725
            // Return the actual ID we forced on insert as a surrogate.
8726
            return $suggestedUid;
8727
        }
8728
        if ($connection->getDatabasePlatform() instanceof SQLServerPlatform) {
8729
            return $this->postProcessSqlServerInsert($connection, $tableName);
8730
        }
8731
        $id = $connection->lastInsertId($tableName);
8732
        return (int)$id;
8733
    }
8734
8735
    /**
8736
     * Get the last insert ID from sql server
8737
     *
8738
     * - first checks whether doctrine might be able to fetch the ID from the
8739
     * sequence table
8740
     * - if that does not succeed it manually selects the current IDENTITY value
8741
     * from a table
8742
     * - returns 0 if both fail
8743
     *
8744
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8745
     * @param string $tableName
8746
     * @return int
8747
     * @throws \Doctrine\DBAL\DBALException
8748
     */
8749
    protected function postProcessSqlServerInsert(Connection $connection, string $tableName): int
8750
    {
8751
        $id = $connection->lastInsertId($tableName);
8752
        if (!((int)$id > 0)) {
8753
            $table = $connection->quoteIdentifier($tableName);
8754
            $result = $connection->executeQuery('SELECT IDENT_CURRENT(\'' . $table . '\') AS id')->fetch();
8755
            if (isset($result['id']) && $result['id'] > 0) {
8756
                $id = $result['id'];
8757
            }
8758
        }
8759
        return (int)$id;
8760
    }
8761
8762
    /**
8763
     * PostgreSQL works with sequences for auto increment columns. A sequence is not updated when a value is
8764
     * written to such a column. To avoid clashes when the sequence returns an existing ID this helper will
8765
     * update the sequence to the current max value of the column.
8766
     *
8767
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8768
     * @param string $tableName
8769
     */
8770
    protected function postProcessPostgresqlInsert(Connection $connection, string $tableName)
8771
    {
8772
        $queryBuilder = $connection->createQueryBuilder();
8773
        $queryBuilder->getRestrictions()->removeAll();
8774
        $row = $queryBuilder->select('PGT.schemaname', 'S.relname', 'C.attname', 'T.relname AS tablename')
8775
            ->from('pg_class', 'S')
8776
            ->from('pg_depend', 'D')
8777
            ->from('pg_class', 'T')
8778
            ->from('pg_attribute', 'C')
8779
            ->from('pg_tables', 'PGT')
8780
            ->where(
8781
                $queryBuilder->expr()->eq('S.relkind', $queryBuilder->quote('S')),
8782
                $queryBuilder->expr()->eq('S.oid', $queryBuilder->quoteIdentifier('D.objid')),
8783
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('T.oid')),
8784
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('C.attrelid')),
8785
                $queryBuilder->expr()->eq('D.refobjsubid', $queryBuilder->quoteIdentifier('C.attnum')),
8786
                $queryBuilder->expr()->eq('T.relname', $queryBuilder->quoteIdentifier('PGT.tablename')),
8787
                $queryBuilder->expr()->eq('PGT.tablename', $queryBuilder->quote($tableName))
8788
            )
8789
            ->setMaxResults(1)
8790
            ->execute()
8791
            ->fetch();
8792
8793
        if ($row !== false) {
8794
            $connection->exec(
8795
                sprintf(
8796
                    'SELECT SETVAL(%s, COALESCE(MAX(%s), 0)+1, FALSE) FROM %s',
8797
                    $connection->quote($row['schemaname'] . '.' . $row['relname']),
8798
                    $connection->quoteIdentifier($row['attname']),
8799
                    $connection->quoteIdentifier($row['schemaname'] . '.' . $row['tablename'])
8800
                )
8801
            );
8802
        }
8803
    }
8804
8805
    /**
8806
     * Return the cache entry identifier for field evals
8807
     *
8808
     * @param string $additionalIdentifier
8809
     * @return string
8810
     */
8811
    protected function getFieldEvalCacheIdentifier($additionalIdentifier)
8812
    {
8813
        return 'core-datahandler-eval-' . md5($additionalIdentifier);
8814
    }
8815
8816
    /**
8817
     * @return RelationHandler
8818
     */
8819
    protected function createRelationHandlerInstance()
8820
    {
8821
        $isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces');
8822
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
8823
        $relationHandler->setWorkspaceId($this->BE_USER->workspace);
8824
        $relationHandler->setUseLiveReferenceIds($isWorkspacesLoaded);
8825
        $relationHandler->setUseLiveParentIds($isWorkspacesLoaded);
8826
        return $relationHandler;
8827
    }
8828
8829
    /**
8830
     * Create and returns an instance of the CacheManager
8831
     *
8832
     * @return CacheManager
8833
     */
8834
    protected function getCacheManager()
8835
    {
8836
        return GeneralUtility::makeInstance(CacheManager::class);
8837
    }
8838
8839
    /**
8840
     * Gets the resourceFactory
8841
     *
8842
     * @return ResourceFactory
8843
     */
8844
    protected function getResourceFactory()
8845
    {
8846
        return GeneralUtility::makeInstance(ResourceFactory::class);
8847
    }
8848
8849
    /**
8850
     * @return LanguageService
8851
     */
8852
    protected function getLanguageService()
8853
    {
8854
        return $GLOBALS['LANG'];
8855
    }
8856
8857
    /**
8858
     * @internal should only be used from within TYPO3 Core
8859
     * @return array
8860
     */
8861
    public function getHistoryRecords(): array
8862
    {
8863
        return $this->historyRecords;
8864
    }
8865
}
8866