Passed
Branch master (6c65a4)
by Christian
27:15 queued 11:09
created

DataHandler::insertNewCopyVersion()   C

Complexity

Conditions 7
Paths 32

Size

Total Lines 35
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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

1172
                list($tscPID) = BackendUtility::getTSCpid($table, $id, /** @scrutinizer ignore-type */ $old_pid_value ? $old_pid_value : $fieldArray['pid']);
Loading history...
1173
                if ($status === 'new' && $table === 'pages') {
1174
                    $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
1175
                    if (isset($TSConfig['permissions.']) && is_array($TSConfig['permissions.'])) {
1176
                        $fieldArray = $this->setTSconfigPermissions($fieldArray, $TSConfig['permissions.']);
1177
                    }
1178
                }
1179
                // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1180
                $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1181
                $newVersion_placeholderFieldArray = [];
1182
                if ($createNewVersion) {
1183
                    // create a placeholder array with already processed field content
1184
                    $newVersion_placeholderFieldArray = $fieldArray;
1185
                }
1186
                // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations / file uploads to field!
1187
                // Forcing some values unto field array:
1188
                // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1189
                $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1190
                if ($createNewVersion) {
1191
                    $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1192
                }
1193
                // Setting system fields
1194
                if ($status === 'new') {
1195
                    if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1196
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1197
                        if ($createNewVersion) {
1198
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1199
                        }
1200
                    }
1201
                    if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1202
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1203
                        if ($createNewVersion) {
1204
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1205
                        }
1206
                    }
1207
                } elseif ($this->checkSimilar) {
1208
                    // Removing fields which are equal to the current value:
1209
                    $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1210
                }
1211
                if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1212
                    $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1213
                    if ($createNewVersion) {
1214
                        $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1215
                    }
1216
                }
1217
                // Set stage to "Editing" to make sure we restart the workflow
1218
                if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1219
                    $fieldArray['t3ver_stage'] = 0;
1220
                }
1221
                // Hook: processDatamap_postProcessFieldArray
1222
                foreach ($hookObjectsArr as $hookObj) {
1223
                    if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1224
                        $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1225
                    }
1226
                }
1227
                // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1228
                // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already and files could have been uploaded that are now "lost"
1229
                if (is_array($fieldArray)) {
1230
                    if ($status === 'new') {
1231
                        if ($table === 'pages') {
1232
                            // for new pages always a refresh is needed
1233
                            $this->pagetreeNeedsRefresh = true;
1234
                        }
1235
1236
                        // This creates a new version of the record with online placeholder and offline version
1237
                        if ($createNewVersion) {
1238
                            // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1239
                            $this->pagetreeNeedsRefresh = true;
1240
1241
                            $newVersion_placeholderFieldArray['t3ver_label'] = 'INITIAL PLACEHOLDER';
1242
                            // Setting placeholder state value for temporary record
1243
                            $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1244
                            // Setting workspace - only so display of place holders can filter out those from other workspaces.
1245
                            $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1246
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $this->getPlaceholderTitleForTableLabel($table);
1247
                            // Saving placeholder as 'original'
1248
                            $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1249
                            // For the actual new offline version, set versioning values to point to placeholder:
1250
                            $fieldArray['pid'] = -1;
1251
                            $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1252
                            $fieldArray['t3ver_id'] = 1;
1253
                            // Setting placeholder state value for version (so it can know it is currently a new version...)
1254
                            $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1255
                            $fieldArray['t3ver_label'] = 'First draft version';
1256
                            $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1257
                            // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1258
                            $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1259
                            if ($phShadowId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $phShadowId of type null|integer 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...
1260
                                // Processes fields of the placeholder record:
1261
                                $this->triggerRemapAction($table, $id, [$this, 'placeholderShadowing'], [$table, $phShadowId]);
1262
                                // Hold auto-versionized ids of placeholders:
1263
                                $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $phShadowId;
1264
                            }
1265
                        } else {
1266
                            $this->insertDB($table, $id, $fieldArray, false, $incomingFieldArray['uid']);
1267
                        }
1268
                    } else {
1269
                        if ($table === 'pages') {
1270
                            // only a certain number of fields needs to be checked for updates
1271
                            // if $this->checkSimilar is TRUE, fields with unchanged values are already removed here
1272
                            $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1273
                            if (!empty($fieldsToCheck)) {
1274
                                $this->pagetreeNeedsRefresh = true;
1275
                            }
1276
                        }
1277
                        $this->updateDB($table, $id, $fieldArray);
1278
                        $this->placeholderShadowing($table, $id);
1279
                    }
1280
                }
1281
                // Hook: processDatamap_afterDatabaseOperations
1282
                // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1283
                // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1284
                $this->hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1285
            }
1286
        }
1287
        // Process the stack of relations to remap/correct
1288
        $this->processRemapStack();
1289
        $this->dbAnalysisStoreExec();
1290
        $this->removeRegisteredFiles();
1291
        // Hook: processDatamap_afterAllOperations
1292
        // Note: When this hook gets called, all operations on the submitted data have been finished.
1293
        foreach ($hookObjectsArr as $hookObj) {
1294
            if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1295
                $hookObj->processDatamap_afterAllOperations($this);
1296
            }
1297
        }
1298
        if ($this->isOuterMostInstance()) {
1299
            $this->processClearCacheQueue();
1300
            $this->resetElementsToBeDeleted();
1301
        }
1302
    }
1303
1304
    /**
1305
     * @param string $table
1306
     * @param string $row
1307
     * @param string $key
1308
     *
1309
     * @return string
1310
     */
1311
    protected function normalizeTimeFormat(string $table, string $value, string $dbType): string
1312
    {
1313
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1314
        $platform = $connection->getDatabasePlatform();
1315
        if ($platform instanceof SQLServerPlatform) {
1316
            $defaultLength = QueryHelper::getDateTimeFormats()[$dbType]['empty'];
1317
            $value = substr(
1318
                $value,
1319
                0,
1320
                strlen($defaultLength)
1321
            );
1322
        }
1323
        return $value;
1324
    }
1325
1326
    /**
1327
     * Sets the "sorting" DB field and the "pid" field of an incoming record that should be added (NEW1234)
1328
     * depending on the record that should be added or where it should be added.
1329
     *
1330
     * This method is called from process_datamap()
1331
     *
1332
     * @param string $table the table name of the record to insert
1333
     * @param int $pid the real PID (numeric) where the record should be
1334
     * @param array $fieldArray field+value pairs to add
1335
     * @return array the modified field array
1336
     */
1337
    protected function resolveSortingAndPidForNewRecord(string $table, int $pid, array $fieldArray): array
1338
    {
1339
        $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
1340
        // Points to a page on which to insert the element, possibly in the top of the page
1341
        if ($pid >= 0) {
1342
            // Ensure that the "pid" is not a translated page ID, but the default page ID
1343
            $pid = $this->getDefaultLanguagePageId($pid);
1344
            // The numerical pid is inserted in the data array
1345
            $fieldArray['pid'] = $pid;
1346
            // If this table is sorted we better find the top sorting number
1347
            if ($sortRow) {
1348
                $fieldArray[$sortRow] = $this->getSortNumber($table, 0, $pid);
1349
            }
1350
        } elseif ($sortRow) {
1351
            // Points to another record before itself
1352
            // If this table is sorted we better find the top sorting number
1353
            // Because $pid is < 0, getSortNumber() returns an array
1354
            $sortingInfo = $this->getSortNumber($table, 0, $pid);
1355
            $fieldArray['pid'] = $sortingInfo['pid'];
1356
            $fieldArray[$sortRow] = $sortingInfo['sortNumber'];
1357
        } else {
1358
            // Here we fetch the PID of the record that we point to
1359
            $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

1359
            $record = $this->recordInfo($table, /** @scrutinizer ignore-type */ abs($pid), 'pid');
Loading history...
1360
            // Ensure that the "pid" is not a translated page ID, but the default page ID
1361
            $fieldArray['pid'] = $this->getDefaultLanguagePageId($record['pid']);
1362
        }
1363
        return $fieldArray;
1364
    }
1365
1366
    /**
1367
     * Fix shadowing of data in case we are editing an offline version of a live "New" placeholder record:
1368
     *
1369
     * @param string $table Table name
1370
     * @param int $id Record uid
1371
     */
1372
    public function placeholderShadowing($table, $id)
1373
    {
1374
        if ($liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, '*')) {
1375
            if (VersionState::cast($liveRec['t3ver_state'])->indicatesPlaceholder()) {
1376
                $justStoredRecord = BackendUtility::getRecord($table, $id);
1377
                $newRecord = [];
1378
                $shadowCols = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1379
                $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1380
                $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1381
                if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
1382
                    $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
1383
                }
1384
                $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['type'];
1385
                $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label'];
1386
                $shadowColumns = array_unique(GeneralUtility::trimExplode(',', $shadowCols, true));
1387
                foreach ($shadowColumns as $fieldName) {
1388
                    if ((string)$justStoredRecord[$fieldName] !== (string)$liveRec[$fieldName] && isset($GLOBALS['TCA'][$table]['columns'][$fieldName]) && $fieldName !== 'uid' && $fieldName !== 'pid') {
1389
                        $newRecord[$fieldName] = $justStoredRecord[$fieldName];
1390
                    }
1391
                }
1392
                if (!empty($newRecord)) {
1393
                    if ($this->enableLogging) {
1394
                        $this->log($table, $liveRec['uid'], 0, 0, 0, 'Shadowing done on fields <i>' . implode(',', array_keys($newRecord)) . '</i> in placeholder record ' . $table . ':' . $liveRec['uid'] . ' (offline version UID=' . $id . ')', -1, [], $this->eventPid($table, $liveRec['uid'], $liveRec['pid']));
1395
                    }
1396
                    $this->updateDB($table, $liveRec['uid'], $newRecord);
1397
                }
1398
            }
1399
        }
1400
    }
1401
1402
    /**
1403
     * Create a placeholder title for the label field that does match the field requirements
1404
     *
1405
     * @param string $table The table name
1406
     * @param string $placeholderContent Placeholder content to be used
1407
     * @return string placeholder value
1408
     */
1409
    public function getPlaceholderTitleForTableLabel($table, $placeholderContent = null)
1410
    {
1411
        if ($placeholderContent === null) {
1412
            $placeholderContent = 'PLACEHOLDER';
1413
        }
1414
1415
        $labelPlaceholder = '[' . $placeholderContent . ', WS#' . $this->BE_USER->workspace . ']';
1416
        $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1417
        if (!isset($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'])) {
1418
            return $labelPlaceholder;
1419
        }
1420
        $evalCodesArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'], true);
1421
        $transformedLabel = $this->checkValue_input_Eval($labelPlaceholder, $evalCodesArray, '');
1422
        return $transformedLabel['value'] ?? $labelPlaceholder;
1423
    }
1424
1425
    /**
1426
     * Filling in the field array
1427
     * $this->excludedTablesAndFields is used to filter fields if needed.
1428
     *
1429
     * @param string $table Table name
1430
     * @param int $id Record ID
1431
     * @param array $fieldArray Default values, Preset $fieldArray with 'pid' maybe (pid and uid will be not be overridden anyway)
1432
     * @param array $incomingFieldArray Is which fields/values you want to set. There are processed and put into $fieldArray if OK
1433
     * @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.
1434
     * @param string $status Is 'new' or 'update'
1435
     * @param int $tscPID TSconfig PID
1436
     * @return array Field Array
1437
     */
1438
    public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID)
1439
    {
1440
        // Initialize:
1441
        $originalLanguageRecord = null;
1442
        $originalLanguage_diffStorage = null;
1443
        $diffStorageFlag = false;
1444
        // Setting 'currentRecord' and 'checkValueRecord':
1445
        if (strpos($id, 'NEW') !== false) {
1446
            // Must have the 'current' array - not the values after processing below...
1447
            $checkValueRecord = $fieldArray;
1448
            // IF $incomingFieldArray is an array, overlay it.
1449
            // 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...
1450
            if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {
1451
                ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);
1452
            }
1453
            $currentRecord = $checkValueRecord;
1454
        } else {
1455
            // We must use the current values as basis for this!
1456
            $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));
1457
            // This is done to make the pid positive for offline versions; Necessary to have diff-view for page translations in workspaces.
1458
            BackendUtility::fixVersioningPid($table, $currentRecord);
1459
        }
1460
1461
        // Get original language record if available:
1462
        if (is_array($currentRecord)
1463
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']
1464
            && $GLOBALS['TCA'][$table]['ctrl']['languageField']
1465
            && $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
1466
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
1467
            && (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0) {
1468
            $originalLanguageRecord = $this->recordInfo($table, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');
1469
            BackendUtility::workspaceOL($table, $originalLanguageRecord);
1470
            $originalLanguage_diffStorage = unserialize($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);
1471
        }
1472
1473
        $this->checkValue_currentRecord = $checkValueRecord;
1474
        // In the following all incoming value-fields are tested:
1475
        // - Are the user allowed to change the field?
1476
        // - Is the field uid/pid (which are already set)
1477
        // - perms-fields for pages-table, then do special things...
1478
        // - If the field is nothing of the above and the field is configured in TCA, the fieldvalues are evaluated by ->checkValue
1479
        // If everything is OK, the field is entered into $fieldArray[]
1480
        foreach ($incomingFieldArray as $field => $fieldValue) {
1481
            if (isset($this->excludedTablesAndFields[$table . '-' . $field]) || $this->data_disableFields[$table][$id][$field]) {
1482
                continue;
1483
            }
1484
1485
            // The field must be editable.
1486
            // Checking if a value for language can be changed:
1487
            $languageDeny = $GLOBALS['TCA'][$table]['ctrl']['languageField'] && (string)$GLOBALS['TCA'][$table]['ctrl']['languageField'] === (string)$field && !$this->BE_USER->checkLanguageAccess($fieldValue);
1488
            if ($languageDeny) {
1489
                continue;
1490
            }
1491
1492
            switch ($field) {
1493
                case 'uid':
1494
                case 'pid':
1495
                    // Nothing happens, already set
1496
                    break;
1497
                case 'perms_userid':
1498
                case 'perms_groupid':
1499
                case 'perms_user':
1500
                case 'perms_group':
1501
                case 'perms_everybody':
1502
                    // Permissions can be edited by the owner or the administrator
1503
                    if ($table === 'pages' && ($this->admin || $status === 'new' || $this->pageInfo($id, 'perms_userid') == $this->userid)) {
1504
                        $value = (int)$fieldValue;
1505
                        switch ($field) {
1506
                            case 'perms_userid':
1507
                            case 'perms_groupid':
1508
                                $fieldArray[$field] = $value;
1509
                                break;
1510
                            default:
1511
                                if ($value >= 0 && $value < (2 ** 5)) {
1512
                                    $fieldArray[$field] = $value;
1513
                                }
1514
                        }
1515
                    }
1516
                    break;
1517
                case 't3ver_oid':
1518
                case 't3ver_id':
1519
                case 't3ver_wsid':
1520
                case 't3ver_state':
1521
                case 't3ver_count':
1522
                case 't3ver_stage':
1523
                case 't3ver_tstamp':
1524
                    // t3ver_label is not here because it CAN be edited as a regular field!
1525
                    break;
1526
                case 'l10n_state':
1527
                    $fieldArray[$field] = $fieldValue;
1528
                    break;
1529
                default:
1530
                    if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
1531
                        // Evaluating the value
1532
                        $res = $this->checkValue($table, $field, $fieldValue, $id, $status, $realPid, $tscPID);
1533
                        if (array_key_exists('value', $res)) {
1534
                            $fieldArray[$field] = $res['value'];
1535
                        }
1536
                        // Add the value of the original record to the diff-storage content:
1537
                        if ($this->updateModeL10NdiffData && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
1538
                            $originalLanguage_diffStorage[$field] = $this->updateModeL10NdiffDataClear ? '' : $originalLanguageRecord[$field];
1539
                            $diffStorageFlag = true;
1540
                        }
1541
                        // If autoversioning is happening we need to perform a nasty hack. The case is parallel to a similar hack inside checkValue_group_select_file().
1542
                        // When a copy or version is made of a record, a search is made for any RTEmagic* images in fields having the "images" soft reference parser applied.
1543
                        // That should be TRUE for RTE fields. If any are found they are duplicated to new names and the file reference in the bodytext is updated accordingly.
1544
                        // However, with auto-versioning the submitted content of the field will just overwrite the corrected values. This leaves a) lost RTEmagic files and b) creates a double reference to the old files.
1545
                        // The only solution I can come up with is detecting when auto versioning happens, then see if any RTEmagic images was copied and if so make a stupid string-replace of the content !
1546
                        if ($this->autoVersioningUpdate === true) {
1547
                            if (is_array($this->RTEmagic_copyIndex[$table][$id][$field])) {
1548
                                foreach ($this->RTEmagic_copyIndex[$table][$id][$field] as $oldRTEmagicName => $newRTEmagicName) {
1549
                                    $fieldArray[$field] = str_replace(' src="' . $oldRTEmagicName . '"', ' src="' . $newRTEmagicName . '"', $fieldArray[$field]);
1550
                                }
1551
                            }
1552
                        }
1553
                    } elseif ($GLOBALS['TCA'][$table]['ctrl']['origUid'] === $field) {
1554
                        // Allow value for original UID to pass by...
1555
                        $fieldArray[$field] = $fieldValue;
1556
                    }
1557
            }
1558
        }
1559
1560
        // Dealing with a page translation, setting "sorting", "pid", "perms_*" to the same values as the original record
1561
        if ($table === 'pages' && is_array($originalLanguageRecord)) {
1562
            $fieldArray['sorting'] = $originalLanguageRecord['sorting'];
1563
            $fieldArray['perms_userid'] = $originalLanguageRecord['perms_userid'];
1564
            $fieldArray['perms_groupid'] = $originalLanguageRecord['perms_groupid'];
1565
            $fieldArray['perms_user'] = $originalLanguageRecord['perms_user'];
1566
            $fieldArray['perms_group'] = $originalLanguageRecord['perms_group'];
1567
            $fieldArray['perms_everybody'] = $originalLanguageRecord['perms_everybody'];
1568
        }
1569
1570
        // Add diff-storage information:
1571
        if ($diffStorageFlag && !isset($fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']])) {
1572
            // 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...
1573
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = serialize($originalLanguage_diffStorage);
1574
        }
1575
        // Return fieldArray
1576
        return $fieldArray;
1577
    }
1578
1579
    /*********************************************
1580
     *
1581
     * Evaluation of input values
1582
     *
1583
     ********************************************/
1584
    /**
1585
     * Evaluates a value according to $table/$field settings.
1586
     * This function is for real database fields - NOT FlexForm "pseudo" fields.
1587
     * NOTICE: Calling this function expects this: 1) That the data is saved! (files are copied and so on) 2) That files registered for deletion IS deleted at the end (with ->removeRegisteredFiles() )
1588
     *
1589
     * @param string $table Table name
1590
     * @param string $field Field name
1591
     * @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.
1592
     * @param string $id The record-uid, mainly - but not exclusively - used for logging
1593
     * @param string $status 'update' or 'new' flag
1594
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted. If $realPid is -1 it means that a new version of the record is being inserted.
1595
     * @param int $tscPID TSconfig PID
1596
     * @return array Returns the evaluated $value as key "value" in this array. Can be checked with isset($res['value']) ...
1597
     */
1598
    public function checkValue($table, $field, $value, $id, $status, $realPid, $tscPID)
1599
    {
1600
        // Result array
1601
        $res = [];
1602
1603
        // Processing special case of field pages.doktype
1604
        if ($table === 'pages' && $field === 'doktype') {
1605
            // If the user may not use this specific doktype, we issue a warning
1606
            if (!($this->admin || GeneralUtility::inList($this->BE_USER->groupData['pagetypes_select'], $value))) {
1607
                if ($this->enableLogging) {
1608
                    $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

1608
                    $propArr = $this->getRecordProperties($table, /** @scrutinizer ignore-type */ $id);
Loading history...
1609
                    $this->log($table, $id, 5, 0, 1, '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

1609
                    $this->log($table, /** @scrutinizer ignore-type */ $id, 5, 0, 1, 'You cannot change the \'doktype\' of page \'%s\' to the desired value.', 1, [$propArr['header']], $propArr['event_pid']);
Loading history...
1610
                }
1611
                return $res;
1612
            }
1613
            if ($status === 'update') {
1614
                // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1615
                $onlyAllowedTables = $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1616
                if ($onlyAllowedTables) {
1617
                    $theWrongTables = $this->doesPageHaveUnallowedTables($id, $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

1617
                    $theWrongTables = $this->doesPageHaveUnallowedTables($id, /** @scrutinizer ignore-type */ $value);
Loading history...
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $page_uid 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

1617
                    $theWrongTables = $this->doesPageHaveUnallowedTables(/** @scrutinizer ignore-type */ $id, $value);
Loading history...
1618
                    if ($theWrongTables) {
1619
                        if ($this->enableLogging) {
1620
                            $propArr = $this->getRecordProperties($table, $id);
1621
                            $this->log($table, $id, 5, 0, 1, '\'doktype\' of page \'%s\' could not be changed because the page contains records from disallowed tables; %s', 2, [$propArr['header'], $theWrongTables], $propArr['event_pid']);
1622
                        }
1623
                        return $res;
1624
                    }
1625
                }
1626
            }
1627
        }
1628
1629
        $curValue = null;
1630
        if ((int)$id !== 0) {
1631
            // Get current value:
1632
            $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

1632
            $curValueRec = $this->recordInfo($table, /** @scrutinizer ignore-type */ $id, $field);
Loading history...
1633
            // isset() won't work here, since values can be NULL
1634
            if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1635
                $curValue = $curValueRec[$field];
1636
            }
1637
        }
1638
1639
        // Getting config for the field
1640
        $tcaFieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1641
1642
        // Create $recFID only for those types that need it
1643
        if (
1644
            $tcaFieldConf['type'] === 'flex'
1645
            || $tcaFieldConf['type'] === 'group' && ($tcaFieldConf['internal_type'] === 'file' || $tcaFieldConf['internal_type'] === 'file_reference')
1646
        ) {
1647
            $recFID = $table . ':' . $id . ':' . $field;
1648
        } else {
1649
            $recFID = null;
1650
        }
1651
1652
        // Perform processing:
1653
        $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $this->uploadedFileArray[$table][$id][$field], $tscPID);
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

1653
        $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, /** @scrutinizer ignore-type */ $id, $curValue, $status, $realPid, $recFID, $field, $this->uploadedFileArray[$table][$id][$field], $tscPID);
Loading history...
1654
        return $res;
1655
    }
1656
1657
    /**
1658
     * Branches out evaluation of a field value based on its type as configured in $GLOBALS['TCA']
1659
     * Can be called for FlexForm pseudo fields as well, BUT must not have $field set if so.
1660
     *
1661
     * @param array $res The result array. The processed value (if any!) is set in the "value" key.
1662
     * @param string $value The value to set.
1663
     * @param array $tcaFieldConf Field configuration from $GLOBALS['TCA']
1664
     * @param string $table Table name
1665
     * @param int $id UID of record
1666
     * @param mixed $curValue Current value of the field
1667
     * @param string $status 'update' or 'new' flag
1668
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted. If $realPid is -1 it means that a new version of the record is being inserted.
1669
     * @param string $recFID Field identifier [table:uid:field] for flexforms
1670
     * @param string $field Field name. Must NOT be set if the call is for a flexform field (since flexforms are not allowed within flexforms).
1671
     * @param array $uploadedFiles
1672
     * @param int $tscPID TSconfig PID
1673
     * @param array $additionalData Additional data to be forwarded to sub-processors
1674
     * @return array Returns the evaluated $value as key "value" in this array.
1675
     */
1676
    public function checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID, array $additionalData = null)
1677
    {
1678
        // Convert to NULL value if defined in TCA
1679
        if ($value === null && !empty($tcaFieldConf['eval']) && GeneralUtility::inList($tcaFieldConf['eval'], 'null')) {
1680
            $res = ['value' => null];
1681
            return $res;
1682
        }
1683
1684
        switch ($tcaFieldConf['type']) {
1685
            case 'text':
1686
                $res = $this->checkValueForText($value, $tcaFieldConf, $table, $id, $realPid, $field);
1687
                break;
1688
            case 'passthrough':
1689
            case 'imageManipulation':
1690
            case 'user':
1691
                $res['value'] = $value;
1692
                break;
1693
            case 'input':
1694
                $res = $this->checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field);
1695
                break;
1696
            case 'check':
1697
                $res = $this->checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1698
                break;
1699
            case 'radio':
1700
                $res = $this->checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1701
                break;
1702
            case 'group':
1703
            case 'select':
1704
                $res = $this->checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field);
1705
                break;
1706
            case 'inline':
1707
                $res = $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
1708
                break;
1709
            case 'flex':
1710
                // FlexForms are only allowed for real fields.
1711
                if ($field) {
1712
                    $res = $this->checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field);
1713
                }
1714
                break;
1715
            default:
1716
                // Do nothing
1717
        }
1718
        $res = $this->checkValueForInternalReferences($res, $value, $tcaFieldConf, $table, $id, $field);
1719
        return $res;
1720
    }
1721
1722
    /**
1723
     * Checks values that are used for internal references. If the provided $value
1724
     * is a NEW-identifier, the direct processing is stopped. Instead, the value is
1725
     * forwarded to the remap-stack to be post-processed and resolved into a proper
1726
     * UID after all data has been resolved.
1727
     *
1728
     * This method considers TCA types that cannot handle and resolve these internal
1729
     * values directly, like 'passthrough', 'none' or 'user'. Values are only modified
1730
     * here if the $field is used as 'transOrigPointerField' or 'translationSource'.
1731
     *
1732
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
1733
     * @param string $value The value to set.
1734
     * @param array $tcaFieldConf Field configuration from TCA
1735
     * @param string $table Table name
1736
     * @param int $id UID of record
1737
     * @param string $field The field name
1738
     * @return array The result array. The processed value (if any!) is set in the "value" key.
1739
     */
1740
    protected function checkValueForInternalReferences(array $res, $value, $tcaFieldConf, $table, $id, $field)
1741
    {
1742
        $relevantFieldNames = [
1743
            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null,
1744
            $GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null,
1745
        ];
1746
1747
        if (
1748
            // in case field is empty
1749
            empty($field)
1750
            // in case the field is not relevant
1751
            || !in_array($field, $relevantFieldNames)
1752
            // in case the 'value' index has been unset already
1753
            || !array_key_exists('value', $res)
1754
            // in case it's not a NEW-identifier
1755
            || strpos($value, 'NEW') === false
1756
        ) {
1757
            return $res;
1758
        }
1759
1760
        $valueArray = [$value];
1761
        $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
1762
        $this->addNewValuesToRemapStackChildIds($valueArray);
1763
        $this->remapStack[] = [
1764
            'args' => [$valueArray, $tcaFieldConf, $id, $table, $field],
1765
            'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 3],
1766
            'field' => $field
1767
        ];
1768
        unset($res['value']);
1769
1770
        return $res;
1771
    }
1772
1773
    /**
1774
     * Evaluate "text" type values.
1775
     *
1776
     * @param string $value The value to set.
1777
     * @param array $tcaFieldConf Field configuration from TCA
1778
     * @param string $table Table name
1779
     * @param int $id UID of record
1780
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted. If $realPid is -1 it means that a new version of the record is being inserted.
1781
     * @param string $field Field name
1782
     * @return array $res The result array. The processed value (if any!) is set in the "value" key.
1783
     */
1784
    protected function checkValueForText($value, $tcaFieldConf, $table, $id, $realPid, $field)
1785
    {
1786
        if (isset($tcaFieldConf['eval']) && $tcaFieldConf['eval'] !== '') {
1787
            $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1788
            $evalCodesArray = $this->runtimeCache->get($cacheId);
1789
            if (!is_array($evalCodesArray)) {
1790
                $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1791
                $this->runtimeCache->set($cacheId, $evalCodesArray);
1792
            }
1793
            $valueArray = $this->checkValue_text_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1794
        } else {
1795
            $valueArray = ['value' => $value];
1796
        }
1797
1798
        // Handle richtext transformations
1799
        if ($this->dontProcessTransformations) {
1800
            return $valueArray;
1801
        }
1802
        $recordType = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1803
        $columnsOverridesConfigOfField = $GLOBALS['TCA'][$table]['types'][$recordType]['columnsOverrides'][$field]['config'] ?? null;
1804
        if ($columnsOverridesConfigOfField) {
1805
            ArrayUtility::mergeRecursiveWithOverrule($tcaFieldConf, $columnsOverridesConfigOfField);
1806
        }
1807
        if (isset($tcaFieldConf['enableRichtext']) && (bool)$tcaFieldConf['enableRichtext'] === true) {
1808
            $richtextConfigurationProvider = GeneralUtility::makeInstance(Richtext::class);
1809
            $richtextConfiguration = $richtextConfigurationProvider->getConfiguration($table, $field, $realPid, $recordType, $tcaFieldConf);
1810
            $parseHTML = GeneralUtility::makeInstance(RteHtmlParser::class);
1811
            $parseHTML->init($table . ':' . $field, $realPid);
1812
            $valueArray['value'] = $parseHTML->RTE_transform($value, [], 'db', $richtextConfiguration);
1813
        }
1814
1815
        return $valueArray;
1816
    }
1817
1818
    /**
1819
     * Evaluate "input" type values.
1820
     *
1821
     * @param string $value The value to set.
1822
     * @param array $tcaFieldConf Field configuration from TCA
1823
     * @param string $table Table name
1824
     * @param int $id UID of record
1825
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted. If $realPid is -1 it means that a new version of the record is being inserted.
1826
     * @param string $field Field name
1827
     * @return array $res The result array. The processed value (if any!) is set in the "value" key.
1828
     */
1829
    protected function checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
1830
    {
1831
        // Handle native date/time fields
1832
        $isDateOrDateTimeField = false;
1833
        $format = '';
1834
        $emptyValue = '';
1835
        $dateTimeTypes = QueryHelper::getDateTimeTypes();
1836
        // normal integer "date" fields (timestamps) are handled in checkValue_input_Eval
1837
        if (isset($tcaFieldConf['dbType']) && in_array($tcaFieldConf['dbType'], $dateTimeTypes, true)) {
1838
            if (empty($value)) {
1839
                $value = null;
1840
            } else {
1841
                $isDateOrDateTimeField = true;
1842
                $dateTimeFormats = QueryHelper::getDateTimeFormats();
1843
                $format = $dateTimeFormats[$tcaFieldConf['dbType']]['format'];
1844
1845
                // Convert the date/time into a timestamp for the sake of the checks
1846
                $emptyValue = $dateTimeFormats[$tcaFieldConf['dbType']]['empty'];
1847
                // We store UTC timestamps in the database, which is what getTimestamp() returns.
1848
                $dateTime = new \DateTime($value);
1849
                $value = $value === $emptyValue ? null : $dateTime->getTimestamp();
1850
            }
1851
        }
1852
        // Secures the string-length to be less than max.
1853
        if ((int)$tcaFieldConf['max'] > 0) {
1854
            $value = mb_substr((string)$value, 0, (int)$tcaFieldConf['max'], 'utf-8');
1855
        }
1856
        // Checking range of value:
1857
        // @todo: The "checkbox" option was removed for type=input, this check could be probably relaxed?
1858
        if ($tcaFieldConf['range'] && $value != $tcaFieldConf['checkbox'] && (int)$value !== (int)$tcaFieldConf['default']) {
1859
            if (isset($tcaFieldConf['range']['upper']) && (int)$value > (int)$tcaFieldConf['range']['upper']) {
1860
                $value = $tcaFieldConf['range']['upper'];
1861
            }
1862
            if (isset($tcaFieldConf['range']['lower']) && (int)$value < (int)$tcaFieldConf['range']['lower']) {
1863
                $value = $tcaFieldConf['range']['lower'];
1864
            }
1865
        }
1866
1867
        if (empty($tcaFieldConf['eval'])) {
1868
            $res = ['value' => $value];
1869
        } else {
1870
            // Process evaluation settings:
1871
            $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1872
            $evalCodesArray = $this->runtimeCache->get($cacheId);
1873
            if (!is_array($evalCodesArray)) {
1874
                $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1875
                $this->runtimeCache->set($cacheId, $evalCodesArray);
1876
            }
1877
1878
            $res = $this->checkValue_input_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1879
            if (isset($tcaFieldConf['dbType']) && isset($res['value']) && !$res['value']) {
1880
                // set the value to null if we have an empty value for a native field
1881
                $res['value'] = null;
1882
            }
1883
1884
            // Process UNIQUE settings:
1885
            // Field is NOT set for flexForms - which also means that uniqueInPid and unique is NOT available for flexForm fields! Also getUnique should not be done for versioning and if PID is -1 ($realPid<0) then versioning is happening...
1886
            if ($field && $realPid >= 0 && !empty($res['value'])) {
1887
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
1888
                    $res['value'] = $this->getUnique($table, $field, $res['value'], $id, $realPid);
1889
                }
1890
                if ($res['value'] && in_array('unique', $evalCodesArray, true)) {
1891
                    $res['value'] = $this->getUnique($table, $field, $res['value'], $id);
1892
                }
1893
            }
1894
        }
1895
1896
        // Handle native date/time fields
1897
        if ($isDateOrDateTimeField) {
1898
            // Convert the timestamp back to a date/time
1899
            $res['value'] = $res['value'] ? gmdate($format, $res['value']) : $emptyValue;
1900
        }
1901
        return $res;
1902
    }
1903
1904
    /**
1905
     * Evaluates 'check' type values.
1906
     *
1907
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
1908
     * @param string $value The value to set.
1909
     * @param array $tcaFieldConf Field configuration from TCA
1910
     * @param string $table Table name
1911
     * @param int $id UID of record
1912
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted. If $realPid is -1 it means that a new version of the record is being inserted.
1913
     * @param string $field Field name
1914
     * @return array Modified $res array
1915
     */
1916
    protected function checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
1917
    {
1918
        $items = $tcaFieldConf['items'];
1919
        if ($tcaFieldConf['itemsProcFunc']) {
1920
            /** @var ItemProcessingService $processingService */
1921
            $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
1922
            $items = $processingService->getProcessingItems(
1923
                $table,
1924
                $realPid,
1925
                $field,
1926
                $this->checkValue_currentRecord,
1927
                $tcaFieldConf,
1928
                $tcaFieldConf['items']
1929
            );
1930
        }
1931
1932
        $itemC = 0;
1933
        if ($items !== null) {
1934
            $itemC = count($items);
1935
        }
1936
        if (!$itemC) {
1937
            $itemC = 1;
1938
        }
1939
        $maxV = (2 ** $itemC) - 1;
1940
        if ($value < 0) {
1941
            // @todo: throw LogicException here? Negative values for checkbox items do not make sense and indicate a coding error.
1942
            $value = 0;
1943
        }
1944
        if ($value > $maxV) {
1945
            // @todo: This case is pretty ugly: If there is an itemsProcFunc registered, and if it returns a dynamic,
1946
            // @todo: changing list of items, then it may happen that a value is transformed and vanished checkboxes
1947
            // @todo: are permanently removed from the value.
1948
            // @todo: Suggestion: Throw an exception instead? Maybe a specific, catchable exception that generates a
1949
            // @todo: error message to the user - dynamic item sets via itemProcFunc on check would be a bad idea anyway.
1950
            $value = $value & $maxV;
1951
        }
1952
        if ($field && $realPid >= 0 && $value > 0 && !empty($tcaFieldConf['eval'])) {
1953
            $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1954
            $otherRecordsWithSameValue = [];
1955
            $maxCheckedRecords = 0;
1956
            if (in_array('maximumRecordsCheckedInPid', $evalCodesArray, true)) {
1957
                $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value, $realPid);
1958
                $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsCheckedInPid'];
1959
            }
1960
            if (in_array('maximumRecordsChecked', $evalCodesArray, true)) {
1961
                $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value);
1962
                $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsChecked'];
1963
            }
1964
1965
            // there are more than enough records with value "1" in the DB
1966
            // if so, set this value to "0" again
1967
            if ($maxCheckedRecords && count($otherRecordsWithSameValue) >= $maxCheckedRecords) {
1968
                $value = 0;
1969
                $this->log($table, $id, 5, 0, 1, '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]);
1970
            }
1971
        }
1972
        $res['value'] = $value;
1973
        return $res;
1974
    }
1975
1976
    /**
1977
     * Evaluates 'radio' type values.
1978
     *
1979
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
1980
     * @param string $value The value to set.
1981
     * @param array $tcaFieldConf Field configuration from TCA
1982
     * @param string $table The table of the record
1983
     * @param int $id The id of the record
1984
     * @param int $pid The pid of the record
1985
     * @param string $field The field to check
1986
     * @return array Modified $res array
1987
     */
1988
    protected function checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
1989
    {
1990
        if (is_array($tcaFieldConf['items'])) {
1991
            foreach ($tcaFieldConf['items'] as $set) {
1992
                if ((string)$set[1] === (string)$value) {
1993
                    $res['value'] = $value;
1994
                    break;
1995
                }
1996
            }
1997
        }
1998
1999
        // if no value was found and an itemsProcFunc is defined, check that for the value
2000
        if ($tcaFieldConf['itemsProcFunc'] && empty($res['value'])) {
2001
            $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
2002
            $processedItems = $processingService->getProcessingItems(
2003
                $table,
2004
                $pid,
2005
                $field,
2006
                $this->checkValue_currentRecord,
2007
                $tcaFieldConf,
2008
                $tcaFieldConf['items']
2009
            );
2010
2011
            foreach ($processedItems as $set) {
2012
                if ((string)$set[1] === (string)$value) {
2013
                    $res['value'] = $value;
2014
                    break;
2015
                }
2016
            }
2017
        }
2018
2019
        return $res;
2020
    }
2021
2022
    /**
2023
     * Evaluates 'group' or 'select' type values.
2024
     *
2025
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2026
     * @param string|array $value The value to set.
2027
     * @param array $tcaFieldConf Field configuration from TCA
2028
     * @param string $table Table name
2029
     * @param int $id UID of record
2030
     * @param mixed $curValue Current value of the field
2031
     * @param string $status 'update' or 'new' flag
2032
     * @param string $recFID Field identifier [table:uid:field] for flexforms
2033
     * @param array $uploadedFiles
2034
     * @param string $field Field name
2035
     * @return array Modified $res array
2036
     */
2037
    protected function checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field)
2038
    {
2039
        // Detecting if value sent is an array and if so, implode it around a comma:
2040
        if (is_array($value)) {
2041
            $value = implode(',', $value);
2042
        }
2043
        // 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.
2044
        // Anyway, this should NOT disturb anything else:
2045
        $value = $this->convNumEntityToByteValue($value);
2046
        // When values are sent as group or select they come as comma-separated values which are exploded by this function:
2047
        $valueArray = $this->checkValue_group_select_explodeSelectGroupValue($value);
2048
        // If multiple is not set, remove duplicates:
2049
        if (!$tcaFieldConf['multiple']) {
2050
            $valueArray = array_unique($valueArray);
2051
        }
2052
        // If an exclusive key is found, discard all others:
2053
        if ($tcaFieldConf['type'] === 'select' && $tcaFieldConf['exclusiveKeys']) {
2054
            $exclusiveKeys = GeneralUtility::trimExplode(',', $tcaFieldConf['exclusiveKeys']);
2055
            foreach ($valueArray as $index => $key) {
2056
                if (in_array($key, $exclusiveKeys, true)) {
2057
                    $valueArray = [$index => $key];
2058
                    break;
2059
                }
2060
            }
2061
        }
2062
        // 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?)
2063
        // 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!!
2064
        $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
2065
        // Checking for select / authMode, removing elements from $valueArray if any of them is not allowed!
2066
        if ($tcaFieldConf['type'] === 'select' && $tcaFieldConf['authMode']) {
2067
            $preCount = count($valueArray);
2068
            foreach ($valueArray as $index => $key) {
2069
                if (!$this->BE_USER->checkAuthMode($table, $field, $key, $tcaFieldConf['authMode'])) {
2070
                    unset($valueArray[$index]);
2071
                }
2072
            }
2073
            // 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.
2074
            if ($preCount && empty($valueArray)) {
2075
                return [];
2076
            }
2077
        }
2078
        // For group types:
2079
        if ($tcaFieldConf['type'] === 'group'
2080
            && in_array($tcaFieldConf['internal_type'], ['file', 'file_reference'], true)) {
2081
            $valueArray = $this->checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFiles, $status, $table, $id, $recFID);
2082
        }
2083
        // For select types which has a foreign table attached:
2084
        $unsetResult = false;
2085
        if (
2086
            $tcaFieldConf['type'] === 'group' && $tcaFieldConf['internal_type'] === 'db'
2087
            || $tcaFieldConf['type'] === 'select' && ($tcaFieldConf['foreign_table'] || isset($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages')
2088
        ) {
2089
            // check, if there is a NEW... id in the value, that should be substituted later
2090
            if (strpos($value, 'NEW') !== false) {
2091
                $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2092
                $this->addNewValuesToRemapStackChildIds($valueArray);
2093
                $this->remapStack[] = [
2094
                    'func' => 'checkValue_group_select_processDBdata',
2095
                    'args' => [$valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field],
2096
                    'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 5],
2097
                    'field' => $field
2098
                ];
2099
                $unsetResult = true;
2100
            } else {
2101
                $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field);
2102
            }
2103
        }
2104
        if (!$unsetResult) {
2105
            $newVal = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
2106
            $res['value'] = $this->castReferenceValue(implode(',', $newVal), $tcaFieldConf);
2107
        } else {
2108
            unset($res['value']);
2109
        }
2110
        return $res;
2111
    }
2112
2113
    /**
2114
     * Applies the filter methods from a column's TCA configuration to a value array.
2115
     *
2116
     * @param array $tcaFieldConfiguration
2117
     * @param array $values
2118
     * @return array|mixed
2119
     * @throws \RuntimeException
2120
     */
2121
    protected function applyFiltersToValues(array $tcaFieldConfiguration, array $values)
2122
    {
2123
        if (empty($tcaFieldConfiguration['filter']) || !is_array($tcaFieldConfiguration['filter'])) {
2124
            return $values;
2125
        }
2126
        foreach ($tcaFieldConfiguration['filter'] as $filter) {
2127
            if (empty($filter['userFunc'])) {
2128
                continue;
2129
            }
2130
            $parameters = $filter['parameters'] ?: [];
2131
            $parameters['values'] = $values;
2132
            $parameters['tcaFieldConfig'] = $tcaFieldConfiguration;
2133
            $values = GeneralUtility::callUserFunction($filter['userFunc'], $parameters, $this);
2134
            if (!is_array($values)) {
2135
                throw new \RuntimeException('Failed calling filter userFunc.', 1336051942);
2136
            }
2137
        }
2138
        return $values;
2139
    }
2140
2141
    /**
2142
     * Handling files for group/select function
2143
     *
2144
     * @param array $valueArray Array of incoming file references. Keys are numeric, values are files (basically, this is the exploded list of incoming files)
2145
     * @param array $tcaFieldConf Configuration array from TCA of the field
2146
     * @param string $curValue Current value of the field
2147
     * @param array $uploadedFileArray Array of uploaded files, if any
2148
     * @param string $status 'update' or 'new' flag
2149
     * @param string $table tablename of record
2150
     * @param int $id UID of record
2151
     * @param string $recFID Field identifier [table:uid:field] for flexforms
2152
     * @return array Modified value array
2153
     *
2154
     * @throws \RuntimeException
2155
     */
2156
    public function checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFileArray, $status, $table, $id, $recFID)
2157
    {
2158
        // If file handling should NOT be bypassed, do processing:
2159
        if (!$this->bypassFileHandling) {
2160
            // If any files are uploaded, add them to value array
2161
            // Numeric index means that there are multiple files
2162
            if (isset($uploadedFileArray[0])) {
2163
                $uploadedFiles = $uploadedFileArray;
2164
            } else {
2165
                // There is only one file
2166
                $uploadedFiles = [$uploadedFileArray];
2167
            }
2168
            foreach ($uploadedFiles as $uploadedFileArray) {
2169
                if (!empty($uploadedFileArray['name']) && $uploadedFileArray['tmp_name'] !== 'none') {
2170
                    $valueArray[] = $uploadedFileArray['tmp_name'];
2171
                    $this->alternativeFileName[$uploadedFileArray['tmp_name']] = $uploadedFileArray['name'];
2172
                }
2173
            }
2174
            // Creating fileFunc object.
2175
            if (!$this->fileFunc) {
2176
                $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
2177
            }
2178
            // Setting permitted extensions.
2179
            $this->fileFunc->setFileExtensionPermissions($tcaFieldConf['allowed'], $tcaFieldConf['disallowed'] ?: '*');
2180
        }
2181
        // If there is an upload folder defined:
2182
        if ($tcaFieldConf['uploadfolder'] && $tcaFieldConf['internal_type'] === 'file') {
2183
            $currentFilesForHistory = null;
2184
            // If filehandling should NOT be bypassed, do processing:
2185
            if (!$this->bypassFileHandling) {
2186
                // For logging..
2187
                $propArr = $this->getRecordProperties($table, $id);
2188
                // Get destination path:
2189
                $dest = PATH_site . $tcaFieldConf['uploadfolder'];
2190
                // If we are updating:
2191
                if ($status === 'update') {
2192
                    // Traverse the input values and convert to absolute filenames in case the update happens to an autoVersionized record.
2193
                    // Background: This is a horrible workaround! The problem is that when a record is auto-versionized the files of the record get copied and therefore get new names which is overridden with the names from the original record in the incoming data meaning both lost files and double-references!
2194
                    // The only solution I could come up with (except removing support for managing files when autoversioning) was to convert all relative files to absolute names so they are copied again (and existing files deleted). This should keep references intact but means that some files are copied, then deleted after being copied _again_.
2195
                    // Actually, the same problem applies to database references in case auto-versioning would include sub-records since in such a case references are remapped - and they would be overridden due to the same principle then.
2196
                    // Illustration of the problem comes here:
2197
                    // We have a record 123 with a file logo.gif. We open and edit the files header in a workspace. So a new version is automatically made.
2198
                    // The versions uid is 456 and the file is copied to "logo_01.gif". But the form data that we sent was based on uid 123 and hence contains the filename "logo.gif" from the original.
2199
                    // The file management code below will do two things: First it will blindly accept "logo.gif" as a file attached to the record (thus creating a double reference) and secondly it will find that "logo_01.gif" was not in the incoming filelist and therefore should be deleted.
2200
                    // If we prefix the incoming file "logo.gif" with its absolute path it will be seen as a new file added. Thus it will be copied to "logo_02.gif". "logo_01.gif" will still be deleted but since the files are the same the difference is zero - only more processing and file copying for no reason. But it will work.
2201
                    if ($this->autoVersioningUpdate === true) {
2202
                        foreach ($valueArray as $key => $theFile) {
2203
                            // If it is an already attached file...
2204
                            if ($theFile === basename($theFile)) {
2205
                                $valueArray[$key] = PATH_site . $tcaFieldConf['uploadfolder'] . '/' . $theFile;
2206
                            }
2207
                        }
2208
                    }
2209
                    // Finding the CURRENT files listed, either from MM or from the current record.
2210
                    $theFileValues = [];
2211
                    // If MM relations for the files also!
2212
                    if ($tcaFieldConf['MM']) {
2213
                        $dbAnalysis = $this->createRelationHandlerInstance();
2214
                        /** @var $dbAnalysis RelationHandler */
2215
                        $dbAnalysis->start('', 'files', $tcaFieldConf['MM'], $id);
2216
                        foreach ($dbAnalysis->itemArray as $item) {
2217
                            if ($item['id']) {
2218
                                $theFileValues[] = $item['id'];
2219
                            }
2220
                        }
2221
                    } else {
2222
                        $theFileValues = GeneralUtility::trimExplode(',', $curValue, true);
2223
                    }
2224
                    $currentFilesForHistory = implode(',', $theFileValues);
2225
                    // DELETE files: If existing files were found, traverse those and register files for deletion which has been removed:
2226
                    if (!empty($theFileValues)) {
2227
                        // Traverse the input values and for all input values which match an EXISTING value, remove the existing from $theFileValues array (this will result in an array of all the existing files which should be deleted!)
2228
                        foreach ($valueArray as $key => $theFile) {
2229
                            if ($theFile && strpos(GeneralUtility::fixWindowsFilePath($theFile), '/') === false) {
2230
                                $theFileValues = ArrayUtility::removeArrayEntryByValue($theFileValues, $theFile);
2231
                            }
2232
                        }
2233
                        // This array contains the filenames in the uploadfolder that should be deleted:
2234
                        foreach ($theFileValues as $key => $theFile) {
2235
                            $theFile = trim($theFile);
2236
                            if (@is_file($dest . '/' . $theFile)) {
2237
                                $this->removeFilesStore[] = $dest . '/' . $theFile;
2238
                            } elseif ($theFile) {
2239
                                $this->log($table, $id, 5, 0, 1, 'Could not delete file \'%s\' (does not exist). (%s)', 10, [$dest . '/' . $theFile, $recFID], $propArr['event_pid']);
2240
                            }
2241
                        }
2242
                    }
2243
                }
2244
                // Traverse the submitted values:
2245
                foreach ($valueArray as $key => $theFile) {
2246
                    // Init:
2247
                    $maxSize = (int)$tcaFieldConf['max_size'];
2248
                    // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2249
                    $theDestFile = '';
2250
                    // a FAL file was added, now resolve the file object and get the absolute path
2251
                    // @todo in future versions this needs to be modified to handle FAL objects natively
2252
                    if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2253
                        $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2254
                        $theFile = $fileObject->getForLocalProcessing(false);
2255
                    }
2256
                    // NEW FILES? If the value contains '/' it indicates, that the file
2257
                    // is new and should be added to the uploadsdir (whether its absolute or relative does not matter here)
2258
                    if (strpos(GeneralUtility::fixWindowsFilePath($theFile), '/') !== false) {
2259
                        // Check various things before copying file:
2260
                        // File and destination must exist
2261
                        if (@is_dir($dest) && (@is_file($theFile) || @is_uploaded_file($theFile))) {
2262
                            // Finding size.
2263
                            if (is_uploaded_file($theFile) && $theFile == $uploadedFileArray['tmp_name']) {
2264
                                $fileSize = $uploadedFileArray['size'];
2265
                            } else {
2266
                                $fileSize = filesize($theFile);
2267
                            }
2268
                            // Check file size:
2269
                            if (!$maxSize || $fileSize <= $maxSize * 1024) {
2270
                                // Prepare filename:
2271
                                $theEndFileName = $this->alternativeFileName[$theFile] ?? $theFile;
2272
                                $fI = GeneralUtility::split_fileref($theEndFileName);
2273
                                // Check for allowed extension:
2274
                                if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2275
                                    $theDestFile = $this->fileFunc->getUniqueName($this->fileFunc->cleanFileName($fI['file']), $dest);
2276
                                    // If we have a unique destination filename, then write the file:
2277
                                    if ($theDestFile) {
2278
                                        GeneralUtility::upload_copy_move($theFile, $theDestFile);
2279
                                        // Hook for post-processing the upload action
2280
                                        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'] ?? [] as $className) {
2281
                                            $hookObject = GeneralUtility::makeInstance($className);
2282
                                            if (!$hookObject instanceof DataHandlerProcessUploadHookInterface) {
2283
                                                throw new \UnexpectedValueException($className . ' must implement interface ' . DataHandlerProcessUploadHookInterface::class, 1279962349);
2284
                                            }
2285
                                            $hookObject->processUpload_postProcessAction($theDestFile, $this);
2286
                                        }
2287
                                        $this->copiedFileMap[$theFile] = $theDestFile;
2288
                                        clearstatcache();
2289
                                        if (!@is_file($theDestFile)) {
2290
                                            $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: The destination path (%s) may be write protected. Please make it write enabled!. (%s)', 16, [$theFile, dirname($theDestFile), $recFID], $propArr['event_pid']);
2291
                                        }
2292
                                    } else {
2293
                                        $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, [$theFile, $theDestFile, $recFID], $propArr['event_pid']);
2294
                                    }
2295
                                } else {
2296
                                    $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2297
                                }
2298
                            } else {
2299
                                $this->log($table, $id, 5, 0, 1, 'Filesize (%s) of file \'%s\' exceeds limit (%s). (%s)', 13, [GeneralUtility::formatSize($fileSize), $theFile, GeneralUtility::formatSize($maxSize * 1024), $recFID], $propArr['event_pid']);
2300
                            }
2301
                        } else {
2302
                            $this->log($table, $id, 5, 0, 1, 'The destination (%s) or the source file (%s) does not exist. (%s)', 14, [$dest, $theFile, $recFID], $propArr['event_pid']);
2303
                        }
2304
                        // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2305
                        if (@is_file($theDestFile)) {
2306
                            $info = GeneralUtility::split_fileref($theDestFile);
2307
                            // The value is set to the new filename
2308
                            $valueArray[$key] = $info['file'];
2309
                        } else {
2310
                            // The value is set to the new filename
2311
                            unset($valueArray[$key]);
2312
                        }
2313
                    }
2314
                }
2315
            }
2316
            // If MM relations for the files, we will set the relations as MM records and change the valuearray to contain a single entry with a count of the number of files!
2317
            if ($tcaFieldConf['MM']) {
2318
                /** @var $dbAnalysis RelationHandler */
2319
                $dbAnalysis = $this->createRelationHandlerInstance();
2320
                // Dummy
2321
                $dbAnalysis->tableArray['files'] = [];
2322
                foreach ($valueArray as $key => $theFile) {
2323
                    // Explode files
2324
                    $dbAnalysis->itemArray[]['id'] = $theFile;
2325
                }
2326
                if ($status === 'update') {
2327
                    $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, 0);
2328
                    $newFiles = implode(',', $dbAnalysis->getValueArray());
2329
                    list(, , $recFieldName) = explode(':', $recFID);
2330
                    if ($currentFilesForHistory != $newFiles) {
2331
                        $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = $currentFilesForHistory;
2332
                        $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = $newFiles;
2333
                    } else {
2334
                        $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = '';
2335
                        $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = '';
2336
                    }
2337
                } else {
2338
                    $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, 0];
2339
                }
2340
                $valueArray = $dbAnalysis->countItems();
2341
            }
2342
        } else {
2343
            if (!empty($valueArray)) {
2344
                // If filehandling should NOT be bypassed, do processing:
2345
                if (!$this->bypassFileHandling) {
2346
                    // For logging..
2347
                    $propArr = $this->getRecordProperties($table, $id);
2348
                    foreach ($valueArray as &$theFile) {
2349
                        // FAL handling: it's a UID, thus it is resolved to the absolute path
2350
                        if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2351
                            $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2352
                            $theFile = $fileObject->getForLocalProcessing(false);
2353
                        }
2354
                        if ($this->alternativeFilePath[$theFile]) {
2355
                            // If alternative File Path is set for the file, then it was an import
2356
                            // don't import the file if it already exists
2357
                            if (@is_file((PATH_site . $this->alternativeFilePath[$theFile]))) {
2358
                                $theFile = PATH_site . $this->alternativeFilePath[$theFile];
2359
                            } elseif (@is_file($theFile)) {
2360
                                $dest = dirname(PATH_site . $this->alternativeFilePath[$theFile]);
2361
                                if (!@is_dir($dest)) {
2362
                                    GeneralUtility::mkdir_deep($dest);
2363
                                }
2364
                                // Init:
2365
                                $maxSize = (int)$tcaFieldConf['max_size'];
2366
                                // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2367
                                $theDestFile = '';
2368
                                $fileSize = filesize($theFile);
2369
                                // Check file size:
2370
                                if (!$maxSize || $fileSize <= $maxSize * 1024) {
2371
                                    // Prepare filename:
2372
                                    $theEndFileName = $this->alternativeFileName[$theFile] ?? $theFile;
2373
                                    $fI = GeneralUtility::split_fileref($theEndFileName);
2374
                                    // Check for allowed extension:
2375
                                    if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2376
                                        $theDestFile = PATH_site . $this->alternativeFilePath[$theFile];
2377
                                        // Write the file:
2378
                                        if ($theDestFile) {
2379
                                            GeneralUtility::upload_copy_move($theFile, $theDestFile);
2380
                                            $this->copiedFileMap[$theFile] = $theDestFile;
2381
                                            clearstatcache();
2382
                                            if (!@is_file($theDestFile)) {
2383
                                                $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: The destination path (%s) may be write protected. Please make it write enabled!. (%s)', 16, [$theFile, dirname($theDestFile), $recFID], $propArr['event_pid']);
2384
                                            }
2385
                                        } else {
2386
                                            $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, [$theFile, $theDestFile, $recFID], $propArr['event_pid']);
2387
                                        }
2388
                                    } else {
2389
                                        $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2390
                                    }
2391
                                } else {
2392
                                    $this->log($table, $id, 5, 0, 1, 'Filesize (%s) of file \'%s\' exceeds limit (%s). (%s)', 13, [GeneralUtility::formatSize($fileSize), $theFile, GeneralUtility::formatSize($maxSize * 1024), $recFID], $propArr['event_pid']);
2393
                                }
2394
                                // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2395
                                if (@is_file($theDestFile)) {
2396
                                    // The value is set to the new filename
2397
                                    $theFile = $theDestFile;
2398
                                } else {
2399
                                    // The value is set to the new filename
2400
                                    unset($theFile);
2401
                                }
2402
                            }
2403
                        }
2404
                        if (!empty($theFile)) {
2405
                            $theFile = GeneralUtility::fixWindowsFilePath($theFile);
2406
                            if (GeneralUtility::isFirstPartOfStr($theFile, PATH_site)) {
2407
                                $theFile = PathUtility::stripPathSitePrefix($theFile);
2408
                            }
2409
                        }
2410
                    }
2411
                    unset($theFile);
2412
                }
2413
            }
2414
        }
2415
        return $valueArray;
2416
    }
2417
2418
    /**
2419
     * Evaluates 'flex' type values.
2420
     *
2421
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2422
     * @param string|array $value The value to set.
2423
     * @param array $tcaFieldConf Field configuration from TCA
2424
     * @param string $table Table name
2425
     * @param int $id UID of record
2426
     * @param mixed $curValue Current value of the field
2427
     * @param string $status 'update' or 'new' flag
2428
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted. If $realPid is -1 it means that a new version of the record is being inserted.
2429
     * @param string $recFID Field identifier [table:uid:field] for flexforms
2430
     * @param int $tscPID TSconfig PID
2431
     * @param array $uploadedFiles Uploaded files for the field
2432
     * @param string $field Field name
2433
     * @return array Modified $res array
2434
     */
2435
    protected function checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field)
2436
    {
2437
        if (is_array($value)) {
2438
            // This value is necessary for flex form processing to happen on flexform fields in page records when they are copied.
2439
            // Problem: when copying a page, flexform XML comes along in the array for the new record - but since $this->checkValue_currentRecord
2440
            // does not have a uid or pid for that sake, the FlexFormTools->getDataStructureIdentifier() function returns no good DS. For new
2441
            // records we do know the expected PID so therefore we send that with this special parameter. Only active when larger than zero.
2442
            $row = $this->checkValue_currentRecord;
2443
            if ($status === 'new') {
2444
                $row['pid'] = $realPid;
2445
            }
2446
2447
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
2448
2449
            // Get data structure. The methods may throw various exceptions, with some of them being
2450
            // ok in certain scenarios, for instance on new record rows. Those are ok to "eat" here
2451
            // and substitute with a dummy DS.
2452
            $dataStructureArray = [ 'sheets' => [ 'sDEF' => [] ] ];
2453
            try {
2454
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
2455
                    [ 'config' => $tcaFieldConf ],
2456
                    $table,
2457
                    $field,
2458
                    $row
2459
                );
2460
2461
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
2462
            } catch (InvalidParentRowException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2463
            } catch (InvalidParentRowLoopException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2464
            } catch (InvalidParentRowRootException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2465
            } catch (InvalidPointerFieldValueException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2466
            } catch (InvalidIdentifierException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2467
            }
2468
2469
            // Get current value array:
2470
            $currentValueArray = (string)$curValue !== '' ? GeneralUtility::xml2array($curValue) : [];
2471
            if (!is_array($currentValueArray)) {
2472
                $currentValueArray = [];
2473
            }
2474
            // Remove all old meta for languages...
2475
            // Evaluation of input values:
2476
            $value['data'] = $this->checkValue_flex_procInData($value['data'], $currentValueArray['data'], $uploadedFiles['data'], $dataStructureArray, [$table, $id, $curValue, $status, $realPid, $recFID, $tscPID]);
2477
            // Create XML from input value:
2478
            $xmlValue = $this->checkValue_flexArray2Xml($value, true);
2479
2480
            // 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
2481
            // (provided that the current value was already stored IN the charset that the new value is converted to).
2482
            $arrValue = GeneralUtility::xml2array($xmlValue);
2483
2484
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'] ?? [] as $className) {
2485
                $hookObject = GeneralUtility::makeInstance($className);
2486
                if (method_exists($hookObject, 'checkFlexFormValue_beforeMerge')) {
2487
                    $hookObject->checkFlexFormValue_beforeMerge($this, $currentValueArray, $arrValue);
2488
                }
2489
            }
2490
2491
            ArrayUtility::mergeRecursiveWithOverrule($currentValueArray, $arrValue);
0 ignored issues
show
Bug introduced by
It seems like $arrValue can also be of type string; however, parameter $overrule of TYPO3\CMS\Core\Utility\A...RecursiveWithOverrule() 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

2491
            ArrayUtility::mergeRecursiveWithOverrule($currentValueArray, /** @scrutinizer ignore-type */ $arrValue);
Loading history...
2492
            $xmlValue = $this->checkValue_flexArray2Xml($currentValueArray, true);
2493
2494
            // Action commands (sorting order and removals of elements) for flexform sections,
2495
            // see FormEngine for the use of this GP parameter
2496
            $actionCMDs = GeneralUtility::_GP('_ACTION_FLEX_FORMdata');
2497
            if (is_array($actionCMDs[$table][$id][$field]['data'])) {
2498
                $arrValue = GeneralUtility::xml2array($xmlValue);
2499
                $this->_ACTION_FLEX_FORMdata($arrValue['data'], $actionCMDs[$table][$id][$field]['data']);
2500
                $xmlValue = $this->checkValue_flexArray2Xml($arrValue, true);
0 ignored issues
show
Bug introduced by
It seems like $arrValue can also be of type string; however, parameter $array of TYPO3\CMS\Core\DataHandl...ckValue_flexArray2Xml() 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

2500
                $xmlValue = $this->checkValue_flexArray2Xml(/** @scrutinizer ignore-type */ $arrValue, true);
Loading history...
2501
            }
2502
            // Create the value XML:
2503
            $res['value'] = '';
2504
            $res['value'] .= $xmlValue;
2505
        } else {
2506
            // Passthrough...:
2507
            $res['value'] = $value;
2508
        }
2509
2510
        return $res;
2511
    }
2512
2513
    /**
2514
     * Converts an array to FlexForm XML
2515
     *
2516
     * @param array $array Array with FlexForm data
2517
     * @param bool $addPrologue If set, the XML prologue is returned as well.
2518
     * @return string Input array converted to XML
2519
     */
2520
    public function checkValue_flexArray2Xml($array, $addPrologue = false)
2521
    {
2522
        /** @var $flexObj FlexFormTools */
2523
        $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
2524
        return $flexObj->flexArray2Xml($array, $addPrologue);
2525
    }
2526
2527
    /**
2528
     * Actions for flex form element (move, delete)
2529
     * allows to remove and move flexform sections
2530
     *
2531
     * @param array $valueArray by reference
2532
     * @param array $actionCMDs
2533
     */
2534
    protected function _ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
2535
    {
2536
        if (!is_array($valueArray) || !is_array($actionCMDs)) {
2537
            return;
2538
        }
2539
2540
        foreach ($actionCMDs as $key => $value) {
2541
            if ($key === '_ACTION') {
2542
                // First, check if there are "commands":
2543
                if (current($actionCMDs[$key]) === '') {
2544
                    continue;
2545
                }
2546
2547
                asort($actionCMDs[$key]);
2548
                $newValueArray = [];
2549
                foreach ($actionCMDs[$key] as $idx => $order) {
2550
                    // Just one reflection here: It is clear that when removing elements from a flexform, then we will get lost
2551
                    // files unless we act on this delete operation by traversing and deleting files that were referred to.
2552
                    if ($order !== 'DELETE') {
2553
                        $newValueArray[$idx] = $valueArray[$idx];
2554
                    }
2555
                    unset($valueArray[$idx]);
2556
                }
2557
                $valueArray += $newValueArray;
2558
            } elseif (is_array($actionCMDs[$key]) && isset($valueArray[$key])) {
2559
                $this->_ACTION_FLEX_FORMdata($valueArray[$key], $actionCMDs[$key]);
2560
            }
2561
        }
2562
    }
2563
2564
    /**
2565
     * Evaluates 'inline' type values.
2566
     * (partly copied from the select_group function on this issue)
2567
     *
2568
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2569
     * @param string $value The value to set.
2570
     * @param array $tcaFieldConf Field configuration from TCA
2571
     * @param array $PP Additional parameters in a numeric array: $table,$id,$curValue,$status,$realPid,$recFID
2572
     * @param string $field Field name
2573
     * @param array $additionalData Additional data to be forwarded to sub-processors
2574
     * @return array Modified $res array
2575
     */
2576
    public function checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData = null)
2577
    {
2578
        list($table, $id, , $status) = $PP;
2579
        $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
2580
    }
2581
2582
    /**
2583
     * Evaluates 'inline' type values.
2584
     * (partly copied from the select_group function on this issue)
2585
     *
2586
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2587
     * @param string $value The value to set.
2588
     * @param array $tcaFieldConf Field configuration from TCA
2589
     * @param string $table Table name
2590
     * @param int $id UID of record
2591
     * @param string $status 'update' or 'new' flag
2592
     * @param string $field Field name
2593
     * @param array $additionalData Additional data to be forwarded to sub-processors
2594
     * @return array|bool Modified $res array
2595
     */
2596
    public function checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData = null)
2597
    {
2598
        if (!$tcaFieldConf['foreign_table']) {
2599
            // Fatal error, inline fields should always have a foreign_table defined
2600
            return false;
2601
        }
2602
        // When values are sent they come as comma-separated values which are exploded by this function:
2603
        $valueArray = GeneralUtility::trimExplode(',', $value);
2604
        // Remove duplicates: (should not be needed)
2605
        $valueArray = array_unique($valueArray);
2606
        // Example for received data:
2607
        // $value = 45,NEW4555fdf59d154,12,123
2608
        // We need to decide whether we use the stack or can save the relation directly.
2609
        if (!empty($value) && (strpos($value, 'NEW') !== false || !MathUtility::canBeInterpretedAsInteger($id))) {
2610
            $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2611
            $this->addNewValuesToRemapStackChildIds($valueArray);
2612
            $this->remapStack[] = [
2613
                'func' => 'checkValue_inline_processDBdata',
2614
                'args' => [$valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData],
2615
                'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4],
2616
                'additionalData' => $additionalData,
2617
                'field' => $field,
2618
            ];
2619
            unset($res['value']);
2620
        } elseif ($value || MathUtility::canBeInterpretedAsInteger($id)) {
2621
            $res['value'] = $this->checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData);
2622
        }
2623
        return $res;
2624
    }
2625
2626
    /**
2627
     * Checks if a fields has more items than defined via TCA in maxitems.
2628
     * If there are more items than allowed, the item list is truncated to the defined number.
2629
     *
2630
     * @param array $tcaFieldConf Field configuration from TCA
2631
     * @param array $valueArray Current value array of items
2632
     * @return array The truncated value array of items
2633
     */
2634
    public function checkValue_checkMax($tcaFieldConf, $valueArray)
2635
    {
2636
        // BTW, checking for min and max items here does NOT make any sense when MM is used because the above function
2637
        // calls will just return an array with a single item (the count) if MM is used... Why didn't I perform the check
2638
        // before? Probably because we could not evaluate the validity of record uids etc... Hmm...
2639
        // NOTE to the comment: It's not really possible to check for too few items, because you must then determine first,
2640
        // if the field is actual used regarding the CType.
2641
        $maxitems = isset($tcaFieldConf['maxitems']) ? (int)$tcaFieldConf['maxitems'] : 99999;
2642
        return array_slice($valueArray, 0, $maxitems);
2643
    }
2644
2645
    /*********************************************
2646
     *
2647
     * Helper functions for evaluation functions.
2648
     *
2649
     ********************************************/
2650
    /**
2651
     * Gets a unique value for $table/$id/$field based on $value
2652
     *
2653
     * @param string $table Table name
2654
     * @param string $field Field name for which $value must be unique
2655
     * @param string $value Value string.
2656
     * @param int $id UID to filter out in the lookup (the record itself...)
2657
     * @param int $newPid If set, the value will be unique for this PID
2658
     * @return string Modified value (if not-unique). Will be the value appended with a number (until 100, then the function just breaks).
2659
     */
2660
    public function getUnique($table, $field, $value, $id, $newPid = 0)
2661
    {
2662
        // If the field is configured in TCA, proceed:
2663
        if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
2664
            $newValue = $value;
2665
            $statement = $this->getUniqueCountStatement($newValue, $table, $field, (int)$id, (int)$newPid);
2666
            // For as long as records with the test-value existing, try again (with incremented numbers appended)
2667
            if ($statement->fetchColumn()) {
2668
                for ($counter = 0; $counter <= 100; $counter++) {
2669
                    $newValue = $value . $counter;
2670
                    $statement->bindValue(1, $newValue);
2671
                    $statement->execute();
2672
                    if (!$statement->fetchColumn()) {
2673
                        break;
2674
                    }
2675
                }
2676
            }
2677
            $value = $newValue;
2678
        }
2679
        return $value;
2680
    }
2681
2682
    /**
2683
     * Gets the count of records for a unique field
2684
     *
2685
     * @param string $value The string value which should be unique
2686
     * @param string $table Table name
2687
     * @param string $field Field name for which $value must be unique
2688
     * @param int $uid UID to filter out in the lookup (the record itself...)
2689
     * @param int $pid If set, the value will be unique for this PID
2690
     * @return \Doctrine\DBAL\Driver\Statement|int Return the prepared statement to check uniqueness
2691
     */
2692
    protected function getUniqueCountStatement(
2693
        string $value,
2694
        string $table,
2695
        string $field,
2696
        int $uid,
2697
        int $pid
2698
    ): Statement {
2699
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2700
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
2701
        $queryBuilder
2702
            ->count('uid')
2703
            ->from($table)
2704
            ->where(
2705
                $queryBuilder->expr()->eq($field, $queryBuilder->createPositionalParameter($value, \PDO::PARAM_STR)),
2706
                $queryBuilder->expr()->neq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT))
2707
            );
2708
        if ($pid !== 0) {
2709
            $queryBuilder->andWhere(
2710
                $queryBuilder->expr()->eq('pid', $queryBuilder->createPositionalParameter($pid, \PDO::PARAM_INT))
2711
            );
2712
        } else {
2713
            // pid>=0 for versioning
2714
            $queryBuilder->andWhere(
2715
                $queryBuilder->expr()->gte('pid', $queryBuilder->createPositionalParameter(0, \PDO::PARAM_INT))
2716
            );
2717
        }
2718
2719
        return $queryBuilder->execute();
2720
    }
2721
2722
    /**
2723
     * gets all records that have the same value in a field
2724
     * excluding the given uid
2725
     *
2726
     * @param string $tableName Table name
2727
     * @param int $uid UID to filter out in the lookup (the record itself...)
2728
     * @param string $fieldName Field name for which $value must be unique
2729
     * @param string $value Value string.
2730
     * @param int $pageId If set, the value will be unique for this PID
2731
     * @return array
2732
     */
2733
    public function getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId = 0)
2734
    {
2735
        $result = [];
2736
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
2737
            return $result;
2738
        }
2739
2740
        $uid = (int)$uid;
2741
        $pageId = (int)$pageId;
2742
2743
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
2744
        $queryBuilder->getRestrictions()
2745
            ->removeAll()
2746
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2747
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2748
2749
        $queryBuilder->select('*')
2750
            ->from($tableName)
2751
            ->where(
2752
                $queryBuilder->expr()->eq(
2753
                    $fieldName,
2754
                    $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
2755
                ),
2756
                $queryBuilder->expr()->neq(
2757
                    'uid',
2758
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
2759
                )
2760
            );
2761
2762
        if ($pageId) {
2763
            $queryBuilder->andWhere(
2764
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT))
2765
            );
2766
        } else {
2767
            $queryBuilder->andWhere(
2768
                $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
2769
            );
2770
        }
2771
2772
        $result = $queryBuilder->execute()->fetchAll();
2773
2774
        return $result;
2775
    }
2776
2777
    /**
2778
     * @param string $value The field value to be evaluated
2779
     * @param array $evalArray Array of evaluations to traverse.
2780
     * @param string $is_in The "is_in" value of the field configuration from TCA
2781
     * @return array
2782
     */
2783
    public function checkValue_text_Eval($value, $evalArray, $is_in)
2784
    {
2785
        $res = [];
2786
        $set = true;
2787
        foreach ($evalArray as $func) {
2788
            switch ($func) {
2789
                case 'trim':
2790
                    $value = trim($value);
2791
                    break;
2792
                case 'required':
2793
                    if (!$value) {
2794
                        $set = false;
2795
                    }
2796
                    break;
2797
                default:
2798
                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2799
                        if (class_exists($func)) {
2800
                            $evalObj = GeneralUtility::makeInstance($func);
2801
                            if (method_exists($evalObj, 'evaluateFieldValue')) {
2802
                                $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2803
                            }
2804
                        }
2805
                    }
2806
            }
2807
        }
2808
        if ($set) {
2809
            $res['value'] = $value;
2810
        }
2811
        return $res;
2812
    }
2813
2814
    /**
2815
     * Evaluation of 'input'-type values based on 'eval' list
2816
     *
2817
     * @param string $value Value to evaluate
2818
     * @param array $evalArray Array of evaluations to traverse.
2819
     * @param string $is_in Is-in string for 'is_in' evaluation
2820
     * @return array Modified $value in key 'value' or empty array
2821
     */
2822
    public function checkValue_input_Eval($value, $evalArray, $is_in)
2823
    {
2824
        $res = [];
2825
        $set = true;
2826
        foreach ($evalArray as $func) {
2827
            switch ($func) {
2828
                case 'int':
2829
                case 'year':
2830
                    $value = (int)$value;
2831
                    break;
2832
                case 'time':
2833
                case 'timesec':
2834
                    // If $value is a pure integer we have the number of seconds, we can store that directly
2835
                    if ($value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
2836
                        // $value is an ISO 8601 date
2837
                        $value = (new \DateTime($value))->getTimestamp();
2838
                    }
2839
                    break;
2840
                case 'date':
2841
                case 'datetime':
2842
                    // If $value is a pure integer we have the number of seconds, we can store that directly
2843
                    if ($value !== null && $value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
2844
                        // The value we receive from JS is an ISO 8601 date, which is always in UTC. (the JS code works like that, on purpose!)
2845
                        // For instance "1999-11-11T11:11:11Z"
2846
                        // Since the user actually specifies the time in the server's local time, we need to mangle this
2847
                        // to reflect the server TZ. So we make this 1999-11-11T11:11:11+0200 (assuming Europe/Vienna here)
2848
                        // In the database we store the date in UTC (1999-11-11T09:11:11Z), hence we take the timestamp of this converted value.
2849
                        // For achieving this we work with timestamps only (which are UTC) and simply adjust it for the
2850
                        // TZ difference.
2851
                        try {
2852
                            // Make the date from JS a timestamp
2853
                            $value = (new \DateTime($value))->getTimestamp();
2854
                        } catch (\Exception $e) {
2855
                            // set the default timezone value to achieve the value of 0 as a result
2856
                            $value = (int)date('Z', 0);
2857
                        }
2858
2859
                        // @todo this hacky part is problematic when it comes to times around DST switch! Add test to prove that this is broken.
2860
                        $value -= date('Z', $value);
2861
                    }
2862
                    break;
2863
                case 'double2':
2864
                    $value = preg_replace('/[^0-9,\\.-]/', '', $value);
2865
                    $negative = $value[0] === '-';
2866
                    $value = strtr($value, [',' => '.', '-' => '']);
2867
                    if (strpos($value, '.') === false) {
2868
                        $value .= '.0';
2869
                    }
2870
                    $valueArray = explode('.', $value);
2871
                    $dec = array_pop($valueArray);
2872
                    $value = implode('', $valueArray) . '.' . $dec;
2873
                    if ($negative) {
2874
                        $value *= -1;
2875
                    }
2876
                    $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

2876
                    $value = number_format(/** @scrutinizer ignore-type */ $value, 2, '.', '');
Loading history...
2877
                    break;
2878
                case 'md5':
2879
                    if (strlen($value) !== 32) {
2880
                        $set = false;
2881
                    }
2882
                    break;
2883
                case 'trim':
2884
                    $value = trim($value);
2885
                    break;
2886
                case 'upper':
2887
                    $value = mb_strtoupper($value, 'utf-8');
2888
                    break;
2889
                case 'lower':
2890
                    $value = mb_strtolower($value, 'utf-8');
2891
                    break;
2892
                case 'required':
2893
                    if (!isset($value) || $value === '') {
2894
                        $set = false;
2895
                    }
2896
                    break;
2897
                case 'is_in':
2898
                    $c = mb_strlen($value);
2899
                    if ($c) {
2900
                        $newVal = '';
2901
                        for ($a = 0; $a < $c; $a++) {
2902
                            $char = mb_substr($value, $a, 1);
2903
                            if (mb_strpos($is_in, $char) !== false) {
2904
                                $newVal .= $char;
2905
                            }
2906
                        }
2907
                        $value = $newVal;
2908
                    }
2909
                    break;
2910
                case 'nospace':
2911
                    $value = str_replace(' ', '', $value);
2912
                    break;
2913
                case 'alpha':
2914
                    $value = preg_replace('/[^a-zA-Z]/', '', $value);
2915
                    break;
2916
                case 'num':
2917
                    $value = preg_replace('/[^0-9]/', '', $value);
2918
                    break;
2919
                case 'alphanum':
2920
                    $value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
2921
                    break;
2922
                case 'alphanum_x':
2923
                    $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
2924
                    break;
2925
                case 'domainname':
2926
                    if (!preg_match('/^[a-z0-9.\\-]*$/i', $value)) {
2927
                        $value = GeneralUtility::idnaEncode($value);
2928
                    }
2929
                    break;
2930
                case 'email':
2931
                    if ((string)$value !== '') {
2932
                        $this->checkValue_input_ValidateEmail($value, $set);
2933
                    }
2934
                    break;
2935
                default:
2936
                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2937
                        if (class_exists($func)) {
2938
                            $evalObj = GeneralUtility::makeInstance($func);
2939
                            if (method_exists($evalObj, 'evaluateFieldValue')) {
2940
                                $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2941
                            }
2942
                        }
2943
                    }
2944
            }
2945
        }
2946
        if ($set) {
2947
            $res['value'] = $value;
2948
        }
2949
        return $res;
2950
    }
2951
2952
    /**
2953
     * If $value is not a valid e-mail address,
2954
     * $set will be set to false and a flash error
2955
     * message will be added
2956
     *
2957
     * @param string $value Value to evaluate
2958
     * @param bool $set TRUE if an update should be done
2959
     * @throws \InvalidArgumentException
2960
     * @throws \TYPO3\CMS\Core\Exception
2961
     */
2962
    protected function checkValue_input_ValidateEmail($value, &$set)
2963
    {
2964
        if (GeneralUtility::validEmail($value)) {
2965
            return;
2966
        }
2967
2968
        $set = false;
2969
        /** @var FlashMessage $message */
2970
        $message = GeneralUtility::makeInstance(
2971
            FlashMessage::class,
2972
            sprintf($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:error.invalidEmail'), $value),
0 ignored issues
show
Bug introduced by
sprintf($this->getLangua...invalidEmail'), $value) of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

2972
            /** @scrutinizer ignore-type */ sprintf($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:error.invalidEmail'), $value),
Loading history...
2973
            '', // header is optional
2974
            FlashMessage::ERROR,
0 ignored issues
show
Bug introduced by
TYPO3\CMS\Core\Messaging\FlashMessage::ERROR of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

2974
            /** @scrutinizer ignore-type */ FlashMessage::ERROR,
Loading history...
2975
            true // whether message should be stored in session
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

2975
            /** @scrutinizer ignore-type */ true // whether message should be stored in session
Loading history...
2976
        );
2977
        /** @var $flashMessageService FlashMessageService */
2978
        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
2979
        $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
2980
    }
2981
2982
    /**
2983
     * Returns data for group/db and select fields
2984
     *
2985
     * @param array $valueArray Current value array
2986
     * @param array $tcaFieldConf TCA field config
2987
     * @param int $id Record id, used for look-up of MM relations (local_uid)
2988
     * @param string $status Status string ('update' or 'new')
2989
     * @param string $type The type, either 'select', 'group' or 'inline'
2990
     * @param string $currentTable Table name, needs to be passed to \TYPO3\CMS\Core\Database\RelationHandler
2991
     * @param string $currentField field name, needs to be set for writing to sys_history
2992
     * @return array Modified value array
2993
     */
2994
    public function checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
2995
    {
2996
        if ($type === 'group') {
2997
            $tables = $tcaFieldConf['allowed'];
2998
        } elseif (!empty($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages') {
2999
            $tables = 'sys_language';
3000
        } else {
3001
            $tables = $tcaFieldConf['foreign_table'];
3002
        }
3003
        $prep = $type === 'group' ? $tcaFieldConf['prepend_tname'] : '';
3004
        $newRelations = implode(',', $valueArray);
3005
        /** @var $dbAnalysis RelationHandler */
3006
        $dbAnalysis = $this->createRelationHandlerInstance();
3007
        $dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
3008
        $dbAnalysis->start($newRelations, $tables, '', 0, $currentTable, $tcaFieldConf);
3009
        if ($tcaFieldConf['MM']) {
3010
            // convert submitted items to use version ids instead of live ids
3011
            // (only required for MM relations in a workspace context)
3012
            $dbAnalysis->convertItemArray();
3013
            if ($status === 'update') {
3014
                /** @var $oldRelations_dbAnalysis RelationHandler */
3015
                $oldRelations_dbAnalysis = $this->createRelationHandlerInstance();
3016
                $oldRelations_dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
3017
                // Db analysis with $id will initialize with the existing relations
3018
                $oldRelations_dbAnalysis->start('', $tables, $tcaFieldConf['MM'], $id, $currentTable, $tcaFieldConf);
3019
                $oldRelations = implode(',', $oldRelations_dbAnalysis->getValueArray());
3020
                $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

3020
                $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, /** @scrutinizer ignore-type */ $prep);
Loading history...
3021
                if ($oldRelations != $newRelations) {
3022
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
3023
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
3024
                } else {
3025
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
3026
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
3027
                }
3028
            } else {
3029
                $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
3030
            }
3031
            $valueArray = $dbAnalysis->countItems();
3032
        } else {
3033
            $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

3033
            $valueArray = $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prep);
Loading history...
3034
        }
3035
        // 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.
3036
        return $valueArray;
3037
    }
3038
3039
    /**
3040
     * Explodes the $value, which is a list of files/uids (group select)
3041
     *
3042
     * @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.
3043
     * @return array The value array.
3044
     */
3045
    public function checkValue_group_select_explodeSelectGroupValue($value)
3046
    {
3047
        $valueArray = GeneralUtility::trimExplode(',', $value, true);
3048
        foreach ($valueArray as &$newVal) {
3049
            $temp = explode('|', $newVal, 2);
3050
            $newVal = str_replace(['|', ','], '', rawurldecode($temp[0]));
3051
        }
3052
        unset($newVal);
3053
        return $valueArray;
3054
    }
3055
3056
    /**
3057
     * Starts the processing the input data for flexforms. This will traverse all sheets / languages and for each it will traverse the sub-structure.
3058
     * See checkValue_flex_procInData_travDS() for more details.
3059
     * 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
3060
     *
3061
     * @param array $dataPart The 'data' part of the INPUT flexform data
3062
     * @param array $dataPart_current The 'data' part of the CURRENT flexform data
3063
     * @param array $uploadedFiles The uploaded files for the 'data' part of the INPUT flexform data
3064
     * @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.
3065
     * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions
3066
     * @param string $callBackFunc Optional call back function, see checkValue_flex_procInData_travDS()  DEPRECATED, use \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools instead for traversal!
3067
     * @param array $workspaceOptions
3068
     * @return array The modified 'data' part.
3069
     * @see checkValue_flex_procInData_travDS()
3070
     */
3071
    public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructure, $pParams, $callBackFunc = '', array $workspaceOptions = [])
3072
    {
3073
        if (is_array($dataPart)) {
3074
            foreach ($dataPart as $sKey => $sheetDef) {
3075
                if (isset($dataStructure['sheets'][$sKey]) && is_array($dataStructure['sheets'][$sKey]) && is_array($sheetDef)) {
3076
                    foreach ($sheetDef as $lKey => $lData) {
3077
                        $this->checkValue_flex_procInData_travDS(
3078
                            $dataPart[$sKey][$lKey],
3079
                            $dataPart_current[$sKey][$lKey],
3080
                            $uploadedFiles[$sKey][$lKey],
3081
                            $dataStructure['sheets'][$sKey]['ROOT']['el'],
3082
                            $pParams,
3083
                            $callBackFunc,
3084
                            $sKey . '/' . $lKey . '/',
3085
                            $workspaceOptions
3086
                        );
3087
                    }
3088
                }
3089
            }
3090
        }
3091
        return $dataPart;
3092
    }
3093
3094
    /**
3095
     * Processing of the sheet/language data array
3096
     * 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.
3097
     *
3098
     * @param array $dataValues New values (those being processed): Multidimensional Data array for sheet/language, passed by reference!
3099
     * @param array $dataValues_current Current values: Multidimensional Data array. May be empty array() if not needed (for callBackFunctions)
3100
     * @param array $uploadedFiles Uploaded files array for sheet/language. May be empty array() if not needed (for callBackFunctions)
3101
     * @param array $DSelements Data structure which fits the data array
3102
     * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions / call back function
3103
     * @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.
3104
     * @param string $structurePath
3105
     * @param array $workspaceOptions
3106
     * @see checkValue_flex_procInData()
3107
     */
3108
    public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions = [])
3109
    {
3110
        if (!is_array($DSelements)) {
3111
            return;
3112
        }
3113
3114
        // For each DS element:
3115
        foreach ($DSelements as $key => $dsConf) {
3116
            // Array/Section:
3117
            if ($DSelements[$key]['type'] === 'array') {
3118
                if (!is_array($dataValues[$key]['el'])) {
3119
                    continue;
3120
                }
3121
3122
                if ($DSelements[$key]['section']) {
3123
                    foreach ($dataValues[$key]['el'] as $ik => $el) {
3124
                        if (!is_array($el)) {
3125
                            continue;
3126
                        }
3127
3128
                        if (!is_array($dataValues_current[$key]['el'])) {
3129
                            $dataValues_current[$key]['el'] = [];
3130
                        }
3131
                        $theKey = key($el);
3132
                        if (!is_array($dataValues[$key]['el'][$ik][$theKey]['el'])) {
3133
                            continue;
3134
                        }
3135
3136
                        $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);
3137
                    }
3138
                } else {
3139
                    if (!isset($dataValues[$key]['el'])) {
3140
                        $dataValues[$key]['el'] = [];
3141
                    }
3142
                    $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/', $workspaceOptions);
3143
                }
3144
            } else {
3145
                // init with value from config for passthrough fields
3146
                if (!empty($dsConf['TCEforms']['config']['type']) && $dsConf['TCEforms']['config']['type'] === 'passthrough') {
3147
                    if (!empty($dataValues_current[$key]['vDEF'])) {
3148
                        // If there is existing value, keep it
3149
                        $dataValues[$key]['vDEF'] = $dataValues_current[$key]['vDEF'];
3150
                    } elseif (
3151
                        !empty($dsConf['TCEforms']['config']['default'])
3152
                        && isset($pParams[1])
3153
                        && !MathUtility::canBeInterpretedAsInteger($pParams[1])
3154
                    ) {
3155
                        // If is new record and a default is specified for field, use it.
3156
                        $dataValues[$key]['vDEF'] = $dsConf['TCEforms']['config']['default'];
3157
                    }
3158
                }
3159
                if (!is_array($dsConf['TCEforms']['config']) || !is_array($dataValues[$key])) {
3160
                    continue;
3161
                }
3162
3163
                foreach ($dataValues[$key] as $vKey => $data) {
3164
                    if ($callBackFunc) {
3165
                        if (is_object($this->callBackObj)) {
3166
                            $res = $this->callBackObj->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
3167
                        } else {
3168
                            $res = $this->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
3169
                        }
3170
                    } else {
3171
                        // Default
3172
                        list($CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID) = $pParams;
3173
3174
                        $additionalData = [
3175
                            'flexFormId' => $CVrecFID,
3176
                            'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
3177
                        ];
3178
3179
                        $res = $this->checkValue_SW([], $dataValues[$key][$vKey], $dsConf['TCEforms']['config'], $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], $CVtscPID, $additionalData);
3180
                    }
3181
                    // Adding the value:
3182
                    if (isset($res['value'])) {
3183
                        $dataValues[$key][$vKey] = $res['value'];
3184
                    }
3185
                    // 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.
3186
                    // 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).
3187
                    if (mb_substr($vKey, -9) !== '.vDEFbase') {
3188
                        if ($this->updateModeL10NdiffData && $GLOBALS['TYPO3_CONF_VARS']['BE']['flexFormXMLincludeDiffBase'] && $vKey !== 'vDEF' && ((string)$dataValues[$key][$vKey] !== (string)$dataValues_current[$key][$vKey] || !isset($dataValues_current[$key][$vKey . '.vDEFbase']) || $this->updateModeL10NdiffData === 'FORCE_FFUPD')) {
3189
                            // 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:
3190
                            if (isset($dataValues[$key]['vDEF'])) {
3191
                                $diffValue = $dataValues[$key]['vDEF'];
3192
                            } else {
3193
                                // If not found (for translators with no access to the default language) we use the one from the current-value data set:
3194
                                $diffValue = $dataValues_current[$key]['vDEF'];
3195
                            }
3196
                            // 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.
3197
                            $dataValues[$key][$vKey . '.vDEFbase'] = $this->updateModeL10NdiffDataClear ? '' : $diffValue;
3198
                        }
3199
                    }
3200
                }
3201
            }
3202
        }
3203
    }
3204
3205
    /**
3206
     * Returns data for inline fields.
3207
     *
3208
     * @param array $valueArray Current value array
3209
     * @param array $tcaFieldConf TCA field config
3210
     * @param int $id Record id
3211
     * @param string $status Status string ('update' or 'new')
3212
     * @param string $table Table name, needs to be passed to \TYPO3\CMS\Core\Database\RelationHandler
3213
     * @param string $field The current field the values are modified for
3214
     * @param array $additionalData Additional data to be forwarded to sub-processors
3215
     * @return string Modified values
3216
     */
3217
    protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, array $additionalData = null)
3218
    {
3219
        $foreignTable = $tcaFieldConf['foreign_table'];
3220
        $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
3221
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3222
        /** @var $dbAnalysis RelationHandler */
3223
        $dbAnalysis = $this->createRelationHandlerInstance();
3224
        $dbAnalysis->start(implode(',', $valueArray), $foreignTable, '', 0, $table, $tcaFieldConf);
3225
        // IRRE with a pointer field (database normalization):
3226
        if ($tcaFieldConf['foreign_field']) {
3227
            // if the record was imported, sorting was also imported, so skip this
3228
            $skipSorting = (bool)$this->callFromImpExp;
3229
            // update record in intermediate table (sorting & pointer uid to parent record)
3230
            $dbAnalysis->writeForeignField($tcaFieldConf, $id, 0, $skipSorting);
3231
            $newValue = $dbAnalysis->countItems(false);
3232
        } elseif ($this->getInlineFieldType($tcaFieldConf) === 'mm') {
3233
            // In order to fully support all the MM stuff, directly call checkValue_group_select_processDBdata instead of repeating the needed code here
3234
            $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
3235
            $newValue = $valueArray[0];
3236
        } else {
3237
            $valueArray = $dbAnalysis->getValueArray();
3238
            // Checking that the number of items is correct:
3239
            $valueArray = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
3240
            $newValue = $this->castReferenceValue(implode(',', $valueArray), $tcaFieldConf);
3241
        }
3242
        return $newValue;
3243
    }
3244
3245
    /*********************************************
3246
     *
3247
     * PROCESSING COMMANDS
3248
     *
3249
     ********************************************/
3250
    /**
3251
     * Processing the cmd-array
3252
     * See "TYPO3 Core API" for a description of the options.
3253
     *
3254
     * @return void|bool
3255
     */
3256
    public function process_cmdmap()
3257
    {
3258
        // Editing frozen:
3259
        if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
3260
            $this->newlog('All editing in this workspace has been frozen!', 1);
3261
            return false;
3262
        }
3263
        // Hook initialization:
3264
        $hookObjectsArr = [];
3265
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
3266
            $hookObj = GeneralUtility::makeInstance($className);
3267
            if (method_exists($hookObj, 'processCmdmap_beforeStart')) {
3268
                $hookObj->processCmdmap_beforeStart($this);
3269
            }
3270
            $hookObjectsArr[] = $hookObj;
3271
        }
3272
        $pasteDatamap = [];
3273
        // Traverse command map:
3274
        foreach ($this->cmdmap as $table => $_) {
3275
            // Check if the table may be modified!
3276
            $modifyAccessList = $this->checkModifyAccessList($table);
3277
            if (!$modifyAccessList) {
3278
                $this->log($table, 0, 2, 0, 1, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
3279
            }
3280
            // Check basic permissions and circumstances:
3281
            if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->cmdmap[$table]) || !$modifyAccessList) {
3282
                continue;
3283
            }
3284
3285
            // Traverse the command map:
3286
            foreach ($this->cmdmap[$table] as $id => $incomingCmdArray) {
3287
                if (!is_array($incomingCmdArray)) {
3288
                    continue;
3289
                }
3290
3291
                if ($table === 'pages') {
3292
                    // for commands on pages do a pagetree-refresh
3293
                    $this->pagetreeNeedsRefresh = true;
3294
                }
3295
3296
                foreach ($incomingCmdArray as $command => $value) {
3297
                    $pasteUpdate = false;
3298
                    if (is_array($value) && isset($value['action']) && $value['action'] === 'paste') {
3299
                        // Extended paste command: $command is set to "move" or "copy"
3300
                        // $value['update'] holds field/value pairs which should be updated after copy/move operation
3301
                        // $value['target'] holds original $value (target of move/copy)
3302
                        $pasteUpdate = $value['update'];
3303
                        $value = $value['target'];
3304
                    }
3305
                    foreach ($hookObjectsArr as $hookObj) {
3306
                        if (method_exists($hookObj, 'processCmdmap_preProcess')) {
3307
                            $hookObj->processCmdmap_preProcess($command, $table, $id, $value, $this, $pasteUpdate);
3308
                        }
3309
                    }
3310
                    // Init copyMapping array:
3311
                    // Must clear this array before call from here to those functions:
3312
                    // Contains mapping information between new and old id numbers.
3313
                    $this->copyMappingArray = [];
3314
                    // process the command
3315
                    $commandIsProcessed = false;
3316
                    foreach ($hookObjectsArr as $hookObj) {
3317
                        if (method_exists($hookObj, 'processCmdmap')) {
3318
                            $hookObj->processCmdmap($command, $table, $id, $value, $commandIsProcessed, $this, $pasteUpdate);
3319
                        }
3320
                    }
3321
                    // Only execute default commands if a hook hasn't been processed the command already
3322
                    if (!$commandIsProcessed) {
3323
                        $procId = $id;
3324
                        $backupUseTransOrigPointerField = $this->useTransOrigPointerField;
3325
                        // Branch, based on command
3326
                        switch ($command) {
3327
                            case 'move':
3328
                                $this->moveRecord($table, $id, $value);
3329
                                break;
3330
                            case 'copy':
3331
                                if ($table === 'pages') {
3332
                                    $this->copyPages($id, $value);
3333
                                } else {
3334
                                    $this->copyRecord($table, $id, $value, true);
3335
                                }
3336
                                $procId = $this->copyMappingArray[$table][$id];
3337
                                break;
3338
                            case 'localize':
3339
                                $this->useTransOrigPointerField = true;
3340
                                $this->localize($table, $id, $value);
3341
                                break;
3342
                            case 'copyToLanguage':
3343
                                $this->useTransOrigPointerField = false;
3344
                                $this->localize($table, $id, $value);
3345
                                break;
3346
                            case 'inlineLocalizeSynchronize':
3347
                                $this->inlineLocalizeSynchronize($table, $id, $value);
3348
                                break;
3349
                            case 'delete':
3350
                                $this->deleteAction($table, $id);
3351
                                break;
3352
                            case 'undelete':
3353
                                $this->undeleteRecord($table, $id);
3354
                                break;
3355
                        }
3356
                        $this->useTransOrigPointerField = $backupUseTransOrigPointerField;
3357
                        if (is_array($pasteUpdate)) {
3358
                            $pasteDatamap[$table][$procId] = $pasteUpdate;
3359
                        }
3360
                    }
3361
                    foreach ($hookObjectsArr as $hookObj) {
3362
                        if (method_exists($hookObj, 'processCmdmap_postProcess')) {
3363
                            $hookObj->processCmdmap_postProcess($command, $table, $id, $value, $this, $pasteUpdate, $pasteDatamap);
3364
                        }
3365
                    }
3366
                    // Merging the copy-array info together for remapping purposes.
3367
                    ArrayUtility::mergeRecursiveWithOverrule($this->copyMappingArray_merged, $this->copyMappingArray);
3368
                }
3369
            }
3370
        }
3371
        /** @var $copyTCE DataHandler */
3372
        $copyTCE = $this->getLocalTCE();
3373
        $copyTCE->start($pasteDatamap, [], $this->BE_USER);
3374
        $copyTCE->process_datamap();
3375
        $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3376
        unset($copyTCE);
3377
3378
        // Finally, before exit, check if there are ID references to remap.
3379
        // This might be the case if versioning or copying has taken place!
3380
        $this->remapListedDBRecords();
3381
        $this->processRemapStack();
3382
        foreach ($hookObjectsArr as $hookObj) {
3383
            if (method_exists($hookObj, 'processCmdmap_afterFinish')) {
3384
                $hookObj->processCmdmap_afterFinish($this);
3385
            }
3386
        }
3387
        if ($this->isOuterMostInstance()) {
3388
            $this->processClearCacheQueue();
3389
            $this->resetNestedElementCalls();
3390
        }
3391
    }
3392
3393
    /*********************************************
3394
     *
3395
     * Cmd: Copying
3396
     *
3397
     ********************************************/
3398
    /**
3399
     * Copying a single record
3400
     *
3401
     * @param string $table Element table
3402
     * @param int $uid Element UID
3403
     * @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
3404
     * @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
3405
     * @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!
3406
     * @param string $excludeFields Commalist of fields to exclude from the copy process (might get default values)
3407
     * @param int $language Language ID (from sys_language table)
3408
     * @param bool $ignoreLocalization If TRUE, any localization routine is skipped
3409
     * @return int|null ID of new record, if any
3410
     */
3411
    public function copyRecord($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '', $language = 0, $ignoreLocalization = false)
3412
    {
3413
        $uid = ($origUid = (int)$uid);
3414
        // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
3415
        if (empty($GLOBALS['TCA'][$table]) || $uid === 0) {
3416
            return null;
3417
        }
3418
        if ($this->isRecordCopied($table, $uid)) {
3419
            if (!empty($overrideValues)) {
3420
                $this->log($table, $uid, 1, 0, 1, 'Repeated attempt to copy record "%s:%s" with override values', -1, [$table, $uid]);
3421
            }
3422
            return null;
3423
        }
3424
3425
        // Fetch record with permission check
3426
        $row = $this->recordInfoWithPermissionCheck($table, $uid, 'show');
3427
3428
        // This checks if the record can be selected which is all that a copy action requires.
3429
        if ($row === false) {
3430
            $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record "%s:%s" which does not exist or you do not have permission to read', -1, [$table, $uid]);
3431
            return null;
3432
        }
3433
3434
        // Check if table is allowed on destination page
3435
        if ($destPid >= 0 && !$this->isTableAllowedForThisPage($destPid, $table)) {
3436
            $this->log($table, $uid, 1, 0, 1, 'Attempt to insert record "%s:%s" on a page (%s) that can\'t store record type.', -1, [$table, $uid, $destPid]);
3437
            return null;
3438
        }
3439
3440
        $fullLanguageCheckNeeded = $table !== 'pages';
3441
        // Used to check language and general editing rights
3442
        if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded)) {
3443
            $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record "%s:%s" without having permissions to do so. [' . $this->BE_USER->errorMsg . '].', -1, [$table, $uid]);
3444
            return null;
3445
        }
3446
3447
        $data = [];
3448
        $nonFields = array_unique(GeneralUtility::trimExplode(',', 'uid,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,t3ver_oid,t3ver_wsid,t3ver_id,t3ver_label,t3ver_state,t3ver_count,t3ver_stage,t3ver_tstamp,' . $excludeFields, true));
3449
        BackendUtility::workspaceOL($table, $row, -99, false);
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type true; however, parameter $row of TYPO3\CMS\Backend\Utilit...dUtility::workspaceOL() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

3449
        BackendUtility::workspaceOL($table, /** @scrutinizer ignore-type */ $row, -99, false);
Loading history...
3450
        $row = BackendUtility::purgeComputedPropertiesFromRecord($row);
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type true; however, parameter $record of TYPO3\CMS\Backend\Utilit...dPropertiesFromRecord() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

3450
        $row = BackendUtility::purgeComputedPropertiesFromRecord(/** @scrutinizer ignore-type */ $row);
Loading history...
3451
3452
        // Initializing:
3453
        $theNewID = StringUtility::getUniqueId('NEW');
3454
        $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3455
        $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3456
        // Getting default data:
3457
        $defaultData = $this->newFieldArray($table);
3458
        // Getting "copy-after" fields if applicable:
3459
        $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

3459
        $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, /** @scrutinizer ignore-type */ abs($destPid), 0) : [];
Loading history...
3460
        // Page TSconfig related:
3461
        // 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...
3462
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
3463
        $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
3464
        $tE = $this->getTableEntries($table, $TSConfig);
3465
        // Traverse ALL fields of the selected record:
3466
        $setDefaultOnCopyArray = array_flip(GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy']));
3467
        foreach ($row as $field => $value) {
3468
            if (!in_array($field, $nonFields, true)) {
3469
                // Get TCA configuration for the field:
3470
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3471
                // Preparation/Processing of the value:
3472
                // "pid" is hardcoded of course:
3473
                // isset() won't work here, since values can be NULL in each of the arrays
3474
                // except setDefaultOnCopyArray, since we exploded that from a string
3475
                if ($field === 'pid') {
3476
                    $value = $destPid;
3477
                } elseif (array_key_exists($field, $overrideValues)) {
3478
                    // Override value...
3479
                    $value = $overrideValues[$field];
3480
                } elseif (array_key_exists($field, $copyAfterFields)) {
3481
                    // Copy-after value if available:
3482
                    $value = $copyAfterFields[$field];
3483
                } elseif ($GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy'] && isset($setDefaultOnCopyArray[$field])) {
3484
                    $value = $defaultData[$field];
3485
                } else {
3486
                    // Hide at copy may override:
3487
                    if ($first && $field == $enableField && $GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] && !$this->neverHideAtCopy && !$tE['disableHideAtCopy']) {
3488
                        $value = 1;
3489
                    }
3490
                    // Prepend label on copy:
3491
                    if ($first && $field == $headerField && $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] && !$tE['disablePrependAtCopy']) {
3492
                        $value = $this->getCopyHeader($table, $this->resolvePid($table, $destPid), $field, $this->clearPrefixFromValue($table, $value), 0);
3493
                    }
3494
                    // Processing based on the TCA config field type (files, references, flexforms...)
3495
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $tscPID, $language);
3496
                }
3497
                // Add value to array.
3498
                $data[$table][$theNewID][$field] = $value;
3499
            }
3500
        }
3501
        // Overriding values:
3502
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3503
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3504
        }
3505
        // Setting original UID:
3506
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3507
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3508
        }
3509
        // Do the copy by simply submitting the array through DataHandler:
3510
        /** @var $copyTCE DataHandler */
3511
        $copyTCE = $this->getLocalTCE();
3512
        $copyTCE->start($data, [], $this->BE_USER);
3513
        $copyTCE->process_datamap();
3514
        // Getting the new UID:
3515
        $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID];
3516
        if ($theNewSQLID) {
3517
            $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3518
            $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3519
            // Keep automatically versionized record information:
3520
            if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3521
                $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3522
            }
3523
        }
3524
        // Copy back the cached TSconfig
3525
        $this->cachedTSconfig = $copyTCE->cachedTSconfig;
3526
        $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3527
        unset($copyTCE);
3528
        if (!$ignoreLocalization && $language == 0) {
3529
            //repointing the new translation records to the parent record we just created
3530
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3531
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = 0;
3532
            $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
3533
        }
3534
3535
        return $theNewSQLID;
3536
    }
3537
3538
    /**
3539
     * Copying pages
3540
     * Main function for copying pages.
3541
     *
3542
     * @param int $uid Page UID to copy
3543
     * @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
3544
     */
3545
    public function copyPages($uid, $destPid)
3546
    {
3547
        // Initialize:
3548
        $uid = (int)$uid;
3549
        $destPid = (int)$destPid;
3550
3551
        $copyTablesAlongWithPage = $this->getAllowedTablesToCopyWhenCopyingAPage();
3552
        // Begin to copy pages if we're allowed to:
3553
        if ($this->admin || in_array('pages', $copyTablesAlongWithPage, true)) {
3554
            // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)
3555
            // This method also copies the localizations of a page
3556
            $theNewRootID = $this->copySpecificPage($uid, $destPid, $copyTablesAlongWithPage, true);
3557
            // If we're going to copy recursively
3558
            if ($theNewRootID && $this->copyTree) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $theNewRootID of type null|integer 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...
3559
                // Get ALL subpages to copy (read-permissions are respected!):
3560
                $CPtable = $this->int_pageTreeInfo([], $uid, (int)$this->copyTree, $theNewRootID);
3561
                // Now copying the subpages:
3562
                foreach ($CPtable as $thePageUid => $thePagePid) {
3563
                    $newPid = $this->copyMappingArray['pages'][$thePagePid];
3564
                    if (isset($newPid)) {
3565
                        $this->copySpecificPage($thePageUid, $newPid, $copyTablesAlongWithPage);
3566
                    } else {
3567
                        $this->log('pages', $uid, 5, 0, 1, 'Something went wrong during copying branch');
3568
                        break;
3569
                    }
3570
                }
3571
            }
3572
        } else {
3573
            $this->log('pages', $uid, 5, 0, 1, 'Attempt to copy page without permission to this table');
3574
        }
3575
    }
3576
3577
    /**
3578
     * Compile a list of tables that should be copied along when a page is about to be copied.
3579
     *
3580
     * First, get the list that the user is allowed to modify (all if admin),
3581
     * and then check against a possible limitation within "DataHandler->copyWhichTables" if not set to "*"
3582
     * to limit the list further down
3583
     *
3584
     * @return array
3585
     */
3586
    protected function getAllowedTablesToCopyWhenCopyingAPage(): array
3587
    {
3588
        // Finding list of tables to copy.
3589
        // These are the tables, the user may modify
3590
        $copyTablesArray = $this->admin ? $this->compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3591
        // If not all tables are allowed then make a list of allowed tables.
3592
        // That is the tables that figure in both allowed tables AND the copyTable-list
3593
        if (strpos($this->copyWhichTables, '*') === false) {
3594
            $definedTablesToCopy = GeneralUtility::trimExplode(',', $this->copyWhichTables, true);
3595
            // Pages are always allowed
3596
            $definedTablesToCopy[] = 'pages';
3597
            $definedTablesToCopy = array_flip($definedTablesToCopy);
3598
            foreach ($copyTablesArray as $k => $table) {
3599
                if (!$table || !isset($definedTablesToCopy[$table])) {
3600
                    unset($copyTablesArray[$k]);
3601
                }
3602
            }
3603
        }
3604
        $copyTablesArray = array_unique($copyTablesArray);
3605
        return $copyTablesArray;
3606
    }
3607
    /**
3608
     * Copying a single page ($uid) to $destPid and all tables in the array copyTablesArray.
3609
     *
3610
     * @param int $uid Page uid
3611
     * @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
3612
     * @param array $copyTablesArray Table on pages to copy along with the page.
3613
     * @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
3614
     * @return int|null The id of the new page, if applicable.
3615
     */
3616
    public function copySpecificPage($uid, $destPid, $copyTablesArray, $first = false)
3617
    {
3618
        // Copy the page itself:
3619
        $theNewRootID = $this->copyRecord('pages', $uid, $destPid, $first);
3620
        // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3621
        if ($theNewRootID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $theNewRootID of type null|integer 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...
3622
            foreach ($copyTablesArray as $table) {
3623
                // All records under the page is copied.
3624
                if ($table && is_array($GLOBALS['TCA'][$table]) && $table !== 'pages') {
3625
                    $fields = ['uid'];
3626
                    $languageField = null;
3627
                    $transOrigPointerField = null;
3628
                    $translationSourceField = null;
3629
                    if (BackendUtility::isTableLocalizable($table)) {
3630
                        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
3631
                        $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3632
                        $fields[] = $languageField;
3633
                        $fields[] = $transOrigPointerField;
3634
                        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3635
                            $translationSourceField = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3636
                            $fields[] = $translationSourceField;
3637
                        }
3638
                    }
3639
                    $isTableWorkspaceEnabled = BackendUtility::isTableWorkspaceEnabled($table);
3640
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3641
                    $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
3642
                    $queryBuilder
3643
                        ->select(...$fields)
3644
                        ->from($table)
3645
                        ->where(
3646
                            $queryBuilder->expr()->eq(
3647
                            'pid',
3648
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3649
                        )
3650
                        );
3651
                    if ($isTableWorkspaceEnabled && (int)$this->BE_USER->workspace === 0) {
3652
                        // Table is workspace enabled, user is in default ws -> add t3ver_wsid=0 restriction
3653
                        $queryBuilder->andWhere(
3654
                            $queryBuilder->expr()->eq(
3655
                                't3ver_wsid',
3656
                                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3657
                            )
3658
                        );
3659
                    } elseif ($isTableWorkspaceEnabled) {
3660
                        // Table is workspace enabled, user has a ws selected -> select wsid=0 and selected wsid rows
3661
                        $queryBuilder->andWhere($queryBuilder->expr()->in(
3662
                            't3ver_wsid',
3663
                            $queryBuilder->createNamedParameter(
3664
                                [0, $this->BE_USER->workspace],
3665
                                Connection::PARAM_INT_ARRAY
3666
                            )
3667
                        ));
3668
                    }
3669
                    if (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby'])) {
3670
                        $queryBuilder->orderBy($GLOBALS['TCA'][$table]['ctrl']['sortby'], 'DESC');
3671
                    }
3672
                    $queryBuilder->addOrderBy('uid');
3673
                    try {
3674
                        $result = $queryBuilder->execute();
3675
                        $rows = [];
3676
                        while ($row = $result->fetch()) {
3677
                            $rows[$row['uid']] = $row;
3678
                        }
3679
                        // Resolve placeholders of workspace versions
3680
                        if (!empty($rows) && (int)$this->BE_USER->workspace !== 0 && $isTableWorkspaceEnabled) {
3681
                            $rows = array_reverse(
3682
                                $this->resolveVersionedRecords(
3683
                                    $table,
3684
                                    implode(',', $fields),
3685
                                    $GLOBALS['TCA'][$table]['ctrl']['sortby'],
3686
                                    array_keys($rows)
3687
                                ),
3688
                                true
3689
                            );
3690
                        }
3691
                        if (is_array($rows)) {
3692
                            $languageSourceMap = [];
3693
                            $overrideValues = $translationSourceField ? [$translationSourceField => 0] : [];
3694
                            $doRemap = false;
3695
                            foreach ($rows as $row) {
3696
                                // Skip localized records that will be processed in
3697
                                // copyL10nOverlayRecords() on copying the default language record
3698
                                $transOrigPointer = $row[$transOrigPointerField];
3699
                                if ($row[$languageField] > 0 && $transOrigPointer > 0 && isset($rows[$transOrigPointer])) {
3700
                                    continue;
3701
                                }
3702
                                // Copying each of the underlying records...
3703
                                $newUid = $this->copyRecord($table, $row['uid'], $theNewRootID, false, $overrideValues);
3704
                                if ($translationSourceField) {
3705
                                    $languageSourceMap[$row['uid']] = $newUid;
3706
                                    if ($row[$languageField] > 0) {
3707
                                        $doRemap = true;
3708
                                    }
3709
                                }
3710
                            }
3711
                            if ($doRemap) {
3712
                                //remap is needed for records in non-default language records in the "free mode"
3713
                                $this->copy_remapTranslationSourceField($table, $rows, $languageSourceMap);
3714
                            }
3715
                        }
3716
                    } catch (DBALException $e) {
3717
                        $databaseErrorMessage = $e->getPrevious()->getMessage();
3718
                        $this->log($table, $uid, 5, 0, 1, 'An SQL error occurred: ' . $databaseErrorMessage);
3719
                    }
3720
                }
3721
            }
3722
            $this->processRemapStack();
3723
            return $theNewRootID;
3724
        }
3725
        return null;
3726
    }
3727
3728
    /**
3729
     * Copying records, but makes a "raw" copy of a record.
3730
     * Basically the only thing observed is field processing like the copying of files and correction of ids. All other fields are 1-1 copied.
3731
     * 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.
3732
     * 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!?
3733
     * This function is used to create new versions of a record.
3734
     * 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.
3735
     *
3736
     * @param string $table Element table
3737
     * @param int $uid Element UID
3738
     * @param int $pid Element PID (real PID, not checked)
3739
     * @param array $overrideArray Override array - must NOT contain any fields not in the table!
3740
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3741
     * @return int Returns the new ID of the record (if applicable)
3742
     */
3743
    public function copyRecord_raw($table, $uid, $pid, $overrideArray = [], array $workspaceOptions = [])
3744
    {
3745
        $uid = (int)$uid;
3746
        // Stop any actions if the record is marked to be deleted:
3747
        // (this can occur if IRRE elements are versionized and child elements are removed)
3748
        if ($this->isElementToBeDeleted($table, $uid)) {
3749
            return null;
3750
        }
3751
        // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3752
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
3753
            return null;
3754
        }
3755
3756
        // Fetch record with permission check
3757
        $row = $this->recordInfoWithPermissionCheck($table, $uid, 'show');
3758
3759
        // This checks if the record can be selected which is all that a copy action requires.
3760
        if ($row === false) {
3761
            $this->log(
3762
                $table,
3763
                $uid,
3764
                3,
3765
                0,
3766
                1,
3767
                'Attempt to rawcopy/versionize record which either does not exist or you don\'t have permission to read'
3768
            );
3769
            return null;
3770
        }
3771
3772
        // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3773
        $nonFields = ['uid', 'pid', 't3ver_id', 't3ver_oid', 't3ver_wsid', 't3ver_label', 't3ver_state', 't3ver_count', 't3ver_stage', 't3ver_tstamp', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody'];
3774
3775
        // Merge in override array.
3776
        $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

3776
        $row = array_merge(/** @scrutinizer ignore-type */ $row, $overrideArray);
Loading history...
3777
        // Traverse ALL fields of the selected record:
3778
        foreach ($row as $field => $value) {
3779
            if (!in_array($field, $nonFields, true)) {
3780
                // Get TCA configuration for the field:
3781
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3782
                if (is_array($conf)) {
3783
                    // Processing based on the TCA config field type (files, references, flexforms...)
3784
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3785
                }
3786
                // Add value to array.
3787
                $row[$field] = $value;
3788
            }
3789
        }
3790
        // Force versioning related fields:
3791
        $row['pid'] = $pid;
3792
        // Setting original UID:
3793
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3794
            $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3795
        }
3796
        // Do the copy by internal function
3797
        $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3798
        if ($theNewSQLID) {
3799
            $this->dbAnalysisStoreExec();
3800
            $this->dbAnalysisStore = [];
3801
            $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3802
            return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3803
        }
3804
        return null;
3805
    }
3806
3807
    /**
3808
     * Inserts a record in the database, passing TCA configuration values through checkValue() but otherwise does NOTHING and checks nothing regarding permissions.
3809
     * 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...
3810
     *
3811
     * @param string $table Table name
3812
     * @param array $fieldArray Field array to insert as a record
3813
     * @param int $realPid The value of PID field.  -1 is indication that we are creating a new version!
3814
     * @return int Returns the new ID of the record (if applicable)
3815
     */
3816
    public function insertNewCopyVersion($table, $fieldArray, $realPid)
3817
    {
3818
        $id = StringUtility::getUniqueId('NEW');
3819
        // $fieldArray is set as current record.
3820
        // 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...
3821
        $this->checkValue_currentRecord = $fieldArray;
3822
        // Makes sure that transformations aren't processed on the copy.
3823
        $backupDontProcessTransformations = $this->dontProcessTransformations;
3824
        $this->dontProcessTransformations = true;
3825
        // Traverse record and input-process each value:
3826
        foreach ($fieldArray as $field => $fieldValue) {
3827
            if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3828
                // Evaluating the value.
3829
                $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0);
3830
                if (isset($res['value'])) {
3831
                    $fieldArray[$field] = $res['value'];
3832
                }
3833
            }
3834
        }
3835
        // System fields being set:
3836
        if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3837
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3838
        }
3839
        if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3840
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3841
        }
3842
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3843
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3844
        }
3845
        // Finally, insert record:
3846
        $this->insertDB($table, $id, $fieldArray, true);
3847
        // Resets dontProcessTransformations to the previous state.
3848
        $this->dontProcessTransformations = $backupDontProcessTransformations;
3849
        // Return new id:
3850
        return $this->substNEWwithIDs[$id];
3851
    }
3852
3853
    /**
3854
     * Processing/Preparing content for copyRecord() function
3855
     *
3856
     * @param string $table Table name
3857
     * @param int $uid Record uid
3858
     * @param string $field Field name being processed
3859
     * @param string $value Input value to be processed.
3860
     * @param array $row Record array
3861
     * @param array $conf TCA field configuration
3862
     * @param int $realDestPid Real page id (pid) the record is copied to
3863
     * @param int $language Language ID (from sys_language table) used in the duplicated record
3864
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3865
     * @return array|string
3866
     * @access private
3867
     * @see copyRecord()
3868
     */
3869
    public function copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = [])
3870
    {
3871
        // Process references and files, currently that means only the files, prepending absolute paths (so the DataHandler engine will detect the file as new and one that should be made into a copy)
3872
        $value = $this->copyRecord_procFilesRefs($conf, $uid, $value);
3873
        $inlineSubType = $this->getInlineFieldType($conf);
3874
        // Get the localization mode for the current (parent) record (keep|select):
3875
        // Register if there are references to take care of or MM is used on an inline field (no change to value):
3876
        if ($this->isReferenceField($conf) || $inlineSubType === 'mm') {
3877
            $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language);
3878
        } elseif ($inlineSubType !== false) {
3879
            $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions, $inlineSubType);
0 ignored issues
show
Unused Code introduced by
The call to TYPO3\CMS\Core\DataHandl...yRecord_processInline() has too many arguments starting with $inlineSubType. ( Ignorable by Annotation )

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

3879
            /** @scrutinizer ignore-call */ 
3880
            $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions, $inlineSubType);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
3880
        }
3881
        // 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())
3882
        if ($conf['type'] === 'flex') {
3883
            // Get current value array:
3884
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
3885
            $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
3886
                [ 'config' => $conf ],
3887
                $table,
3888
                $field,
3889
                $row
3890
            );
3891
            $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
3892
            $currentValueArray = GeneralUtility::xml2array($value);
3893
            // Traversing the XML structure, processing files:
3894
            if (is_array($currentValueArray)) {
3895
                $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
3896
                // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3897
                $value = $currentValueArray;
3898
            }
3899
        }
3900
        return $value;
3901
    }
3902
3903
    /**
3904
     * Processes the children of an MM relation field (select, group, inline) when the parent record is copied.
3905
     *
3906
     * @param string $table
3907
     * @param int $uid
3908
     * @param string $field
3909
     * @param mixed $value
3910
     * @param array $conf
3911
     * @param string $language
3912
     * @return mixed
3913
     */
3914
    protected function copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language)
3915
    {
3916
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
3917
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
3918
        $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3919
        $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3920
        // Localize referenced records of select fields:
3921
        $localizingNonManyToManyFieldReferences = empty($mmTable) && $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3922
        /** @var $dbAnalysis RelationHandler */
3923
        $dbAnalysis = $this->createRelationHandlerInstance();
3924
        $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3925
        $purgeItems = false;
3926
        if ($language > 0 && $localizingNonManyToManyFieldReferences) {
3927
            foreach ($dbAnalysis->itemArray as $index => $item) {
3928
                // Since select fields can reference many records, check whether there's already a localization:
3929
                $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

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

3933
                    $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3934
                }
3935
            }
3936
            $purgeItems = true;
3937
        }
3938
3939
        if ($purgeItems || $mmTable) {
3940
            $dbAnalysis->purgeItemArray();
3941
            $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

3941
            $value = implode(',', $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName));
Loading history...
3942
        }
3943
        // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3944
        if ($value) {
3945
            $this->registerDBList[$table][$uid][$field] = $value;
3946
        }
3947
3948
        return $value;
3949
    }
3950
3951
    /**
3952
     * Processes child records in an inline (IRRE) element when the parent record is copied.
3953
     *
3954
     * @param string $table
3955
     * @param int $uid
3956
     * @param string $field
3957
     * @param mixed $value
3958
     * @param array $row
3959
     * @param array $conf
3960
     * @param int $realDestPid
3961
     * @param string $language
3962
     * @param array $workspaceOptions
3963
     * @return string
3964
     */
3965
    protected function copyRecord_processInline(
3966
        $table,
3967
        $uid,
3968
        $field,
3969
        $value,
3970
        $row,
3971
        $conf,
3972
        $realDestPid,
3973
        $language,
3974
        array $workspaceOptions
3975
    ) {
3976
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3977
        /** @var $dbAnalysis RelationHandler */
3978
        $dbAnalysis = $this->createRelationHandlerInstance();
3979
        $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3980
        // Walk through the items, copy them and remember the new id:
3981
        foreach ($dbAnalysis->itemArray as $k => $v) {
3982
            $newId = null;
3983
            // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3984
            if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3985
                // Children should be localized when the parent gets localized the first time, just do it:
3986
                $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

3986
                $newId = $this->localize($v['table'], $v['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3987
            } else {
3988
                if (!MathUtility::canBeInterpretedAsInteger($realDestPid)) {
3989
                    $newId = $this->copyRecord($v['table'], $v['id'], -$v['id']);
3990
                    // If the destination page id is a NEW string, keep it on the same page
3991
                } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3992
                    // A filled $workspaceOptions indicated that this call
3993
                    // has it's origin in previous versionizeRecord() processing
3994
                    if (!empty($workspaceOptions)) {
3995
                        // Versions use live default id, thus the "new"
3996
                        // id is the original live default child record
3997
                        $newId = $v['id'];
3998
                        $this->versionizeRecord(
3999
                            $v['table'],
4000
                            $v['id'],
4001
                            ($workspaceOptions['label'] ?? 'Auto-created for WS #' . $this->BE_USER->workspace),
4002
                            ($workspaceOptions['delete'] ?? false)
4003
                        );
4004
                        // Otherwise just use plain copyRecord() to create placeholders etc.
4005
                    } else {
4006
                        // If a record has been copied already during this request,
4007
                        // prevent superfluous duplication and use the existing copy
4008
                        if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
4009
                            $newId = $this->copyMappingArray[$v['table']][$v['id']];
4010
                        } else {
4011
                            $newId = $this->copyRecord($v['table'], $v['id'], $realDestPid);
4012
                        }
4013
                    }
4014
                } else {
4015
                    // If a record has been copied already during this request,
4016
                    // prevent superfluous duplication and use the existing copy
4017
                    if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
4018
                        $newId = $this->copyMappingArray[$v['table']][$v['id']];
4019
                    } else {
4020
                        $newId = $this->copyRecord_raw($v['table'], $v['id'], $realDestPid, [], $workspaceOptions);
4021
                    }
4022
                }
4023
            }
4024
            // If the current field is set on a page record, update the pid of related child records:
4025
            if ($table === 'pages') {
4026
                $this->registerDBPids[$v['table']][$v['id']] = $uid;
4027
            } elseif (isset($this->registerDBPids[$table][$uid])) {
4028
                $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][$uid];
4029
            }
4030
            $dbAnalysis->itemArray[$k]['id'] = $newId;
4031
        }
4032
        // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4033
        $value = implode(',', $dbAnalysis->getValueArray());
4034
        $this->registerDBList[$table][$uid][$field] = $value;
4035
4036
        return $value;
4037
    }
4038
4039
    /**
4040
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
4041
     *
4042
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
4043
     * @param array $dsConf TCA field configuration (from Data Structure XML)
4044
     * @param string $dataValue The value of the flexForm field
4045
     * @param string $_1 Not used.
4046
     * @param string $_2 Not used.
4047
     * @param string $_3 Not used.
4048
     * @param array $workspaceOptions
4049
     * @return array Result array with key "value" containing the value of the processing.
4050
     * @see copyRecord(), checkValue_flex_procInData_travDS()
4051
     */
4052
    public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
4053
    {
4054
        // Extract parameters:
4055
        list($table, $uid, $field, $realDestPid) = $pParams;
4056
        // Process references and files, currently that means only the files, prepending absolute paths:
4057
        $dataValue = $this->copyRecord_procFilesRefs($dsConf, $uid, $dataValue);
4058
        // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
4059
        if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
4060
            $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, [], $dsConf, $realDestPid, 0, $workspaceOptions);
4061
            $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
4062
        }
4063
        // Return
4064
        return ['value' => $dataValue];
4065
    }
4066
4067
    /**
4068
     * Modifying a field value for any situation regarding files/references:
4069
     * For attached files: take current file names and prepend absolute paths so they get copied.
4070
     * For DB references: Nothing done.
4071
     *
4072
     * @param array $conf TCE field config
4073
     * @param int $uid Record UID
4074
     * @param string $value Field value (eg. list of files)
4075
     * @return string The (possibly modified) value
4076
     * @see copyRecord(), copyRecord_flexFormCallBack()
4077
     */
4078
    public function copyRecord_procFilesRefs($conf, $uid, $value)
4079
    {
4080
        // Prepend absolute paths to files:
4081
        if ($conf['type'] !== 'group' || ($conf['internal_type'] !== 'file' && $conf['internal_type'] !== 'file_reference')) {
4082
            return $value;
4083
        }
4084
4085
        // Get an array with files as values:
4086
        if ($conf['MM']) {
4087
            $theFileValues = [];
4088
            /** @var $dbAnalysis RelationHandler */
4089
            $dbAnalysis = $this->createRelationHandlerInstance();
4090
            $dbAnalysis->start('', 'files', $conf['MM'], $uid);
4091
            foreach ($dbAnalysis->itemArray as $somekey => $someval) {
4092
                if ($someval['id']) {
4093
                    $theFileValues[] = $someval['id'];
4094
                }
4095
            }
4096
        } else {
4097
            $theFileValues = GeneralUtility::trimExplode(',', $value, true);
4098
        }
4099
        // Traverse this array of files:
4100
        $uploadFolder = $conf['internal_type'] === 'file' ? $conf['uploadfolder'] : '';
4101
        $dest = PATH_site . $uploadFolder;
4102
        $newValue = [];
4103
        foreach ($theFileValues as $file) {
4104
            if (trim($file)) {
4105
                $realFile = str_replace('//', '/', $dest . '/' . trim($file));
4106
                if (@is_file($realFile)) {
4107
                    $newValue[] = $realFile;
4108
                }
4109
            }
4110
        }
4111
        // Implode the new filelist into the new value (all files have absolute paths now which means they will get copied when entering DataHandler as new values...)
4112
        $value = implode(',', $newValue);
4113
4114
        // Return the new value:
4115
        return $value;
4116
    }
4117
4118
    /**
4119
     * Copies any "RTEmagic" image files found in record with table/id to new names.
4120
     * Usage: After copying a record this function should be called to search for "RTEmagic"-images inside the record. If such are found they should be duplicated to new names so all records have a 1-1 relation to them.
4121
     * Reason for copying RTEmagic files: a) if you remove an RTEmagic image from a record it will remove the file - any other record using it will have a lost reference! b) RTEmagic images keeps an original and a copy. The copy always is re-calculated to have the correct physical measures as the HTML tag inserting it defines. This is calculated from the original. Two records using the same image could have difference HTML-width/heights for the image and the copy could only comply with one of them. If you don't want a 1-1 relation you should NOT use RTEmagic files but just insert it as a normal file reference to a file inside fileadmin/ folder
4122
     *
4123
     * @param string $table Table name
4124
     * @param int $theNewSQLID Record UID
4125
     */
4126
    public function copyRecord_fixRTEmagicImages($table, $theNewSQLID)
4127
    {
4128
        // Creating fileFunc object.
4129
        if (!$this->fileFunc) {
4130
            $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
4131
        }
4132
        // Select all RTEmagic files in the reference table from the table/ID
4133
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
4134
        $queryBuilder->getRestrictions()->removeAll();
4135
        $rteFileRecords = $queryBuilder
4136
            ->select('*')
4137
            ->from('sys_refindex')
4138
            ->where(
4139
                $queryBuilder->expr()->eq(
4140
                    'ref_table',
4141
                    $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)
4142
                ),
4143
                $queryBuilder->expr()->like(
4144
                    'ref_string',
4145
                    $queryBuilder->createNamedParameter('%/RTEmagic%', \PDO::PARAM_STR)
4146
                ),
4147
                $queryBuilder->expr()->eq(
4148
                    'softref_key',
4149
                    $queryBuilder->createNamedParameter('images', \PDO::PARAM_STR)
4150
                ),
4151
                $queryBuilder->expr()->eq(
4152
                    'tablename',
4153
                    $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
4154
                ),
4155
                $queryBuilder->expr()->eq(
4156
                    'recuid',
4157
                    $queryBuilder->createNamedParameter($theNewSQLID, \PDO::PARAM_INT)
4158
                )
4159
            )
4160
            ->orderBy('sorting', 'DESC')
4161
            ->execute()
4162
            ->fetchAll();
4163
        // Traverse the files found and copy them:
4164
        if (!is_array($rteFileRecords)) {
4165
            return;
4166
        }
4167
        foreach ($rteFileRecords as $rteFileRecord) {
4168
            $filename = basename($rteFileRecord['ref_string']);
4169
            if (!GeneralUtility::isFirstPartOfStr($filename, 'RTEmagicC_')) {
4170
                continue;
4171
            }
4172
            $fileInfo = [];
4173
            $fileInfo['exists'] = @is_file(PATH_site . $rteFileRecord['ref_string']);
4174
            $fileInfo['original'] = mb_substr($rteFileRecord['ref_string'], 0, -mb_strlen($filename)) . 'RTEmagicP_' . preg_replace('/\\.[[:alnum:]]+$/', '', mb_substr($filename, 10));
4175
            $fileInfo['original_exists'] = @is_file(PATH_site . $fileInfo['original']);
4176
            // CODE from tx_impexp and class.rte_images.php adapted for use here:
4177
            if (!$fileInfo['exists'] || !$fileInfo['original_exists']) {
4178
                $this->newlog('Trying to copy RTEmagic files (' . $rteFileRecord['ref_string'] . ' / ' . $fileInfo['original'] . ') but one or both were missing', 1);
4179
                continue;
4180
            }
4181
            // Initialize; Get directory prefix for file and set the original name:
4182
            $dirPrefix = dirname($rteFileRecord['ref_string']) . '/';
4183
            $rteOrigName = basename($fileInfo['original']);
4184
            // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
4185
            if ($rteOrigName && GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/') && @is_dir(PATH_site . $dirPrefix)) {
4186
                // RTE:
4187
                // From the "original" RTE filename, produce a new "original" destination filename which is unused.
4188
                $origDestName = $this->fileFunc->getUniqueName($rteOrigName, PATH_site . $dirPrefix);
4189
                // Create copy file name:
4190
                $pI = pathinfo($rteFileRecord['ref_string']);
4191
                $copyDestName = dirname($origDestName) . '/RTEmagicC_' . mb_substr(basename($origDestName), 10) . '.' . $pI['extension'];
4192
                if (!@is_file($copyDestName) && !@is_file($origDestName) && $origDestName === GeneralUtility::getFileAbsFileName($origDestName) && $copyDestName === GeneralUtility::getFileAbsFileName($copyDestName)) {
4193
                    // Making copies:
4194
                    GeneralUtility::upload_copy_move(PATH_site . $fileInfo['original'], $origDestName);
4195
                    GeneralUtility::upload_copy_move(PATH_site . $rteFileRecord['ref_string'], $copyDestName);
4196
                    clearstatcache();
4197
                    // Register this:
4198
                    $this->RTEmagic_copyIndex[$rteFileRecord['tablename']][$rteFileRecord['recuid']][$rteFileRecord['field']][$rteFileRecord['ref_string']] = PathUtility::stripPathSitePrefix($copyDestName);
4199
                    // Check and update the record using \TYPO3\CMS\Core\Database\ReferenceIndex
4200
                    if (@is_file($copyDestName)) {
4201
                        /** @var ReferenceIndex $sysRefObj */
4202
                        $sysRefObj = GeneralUtility::makeInstance(ReferenceIndex::class);
4203
                        $sysRefObj->enableRuntimeCache();
4204
                        $error = $sysRefObj->setReferenceValue($rteFileRecord['hash'], PathUtility::stripPathSitePrefix($copyDestName), false, true);
4205
                        if ($error) {
4206
                            $this->newlog(ReferenceIndex::class . '::setReferenceValue(): ' . $error, 1);
4207
                        }
4208
                    } else {
4209
                        $this->newlog('File "' . $copyDestName . '" was not created!', 1);
4210
                    }
4211
                } else {
4212
                    $this->newlog('Could not construct new unique names for file!', 1);
4213
                }
4214
            } else {
4215
                $this->newlog('Maybe directory of file was not within "uploads/"?', 1);
4216
            }
4217
        }
4218
    }
4219
4220
    /**
4221
     * Find l10n-overlay records and perform the requested copy action for these records.
4222
     *
4223
     * @param string $table Record Table
4224
     * @param string $uid UID of the record in the default language
4225
     * @param string $destPid Position to copy to
4226
     * @param bool $first
4227
     * @param array $overrideValues
4228
     * @param string $excludeFields
4229
     */
4230
    public function copyL10nOverlayRecords($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '')
4231
    {
4232
        // There's no need to perform this for tables that are not localizable
4233
        if (!BackendUtility::isTableLocalizable($table)) {
4234
            return;
4235
        }
4236
4237
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4238
        $queryBuilder->getRestrictions()
4239
            ->removeAll()
4240
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4241
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
4242
4243
        $queryBuilder->select('*')
4244
            ->from($table)
4245
            ->where(
4246
                $queryBuilder->expr()->eq(
4247
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
4248
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
4249
                )
4250
            );
4251
4252
        if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4253
            $queryBuilder->andWhere(
4254
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
4255
            );
4256
        }
4257
        // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
4258
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
0 ignored issues
show
Bug introduced by
$destPid of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...:getTSconfig_pidValue(). ( Ignorable by Annotation )

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

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

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

4258
        $tscPID = BackendUtility::getTSconfig_pidValue($table, /** @scrutinizer ignore-type */ $uid, $destPid);
Loading history...
4259
        // Get the localized records to be copied
4260
        $l10nRecords = $queryBuilder->execute()->fetchAll();
4261
        if (is_array($l10nRecords)) {
4262
            $localizedDestPids = [];
4263
            // If $destPid < 0, then it is the uid of the original language record we are inserting after
4264
            if ($destPid < 0) {
4265
                // Get the localized records of the record we are inserting after
4266
                $queryBuilder->setParameter('pointer', abs($destPid), \PDO::PARAM_INT);
4267
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
4268
                // Index the localized record uids by language
4269
                if (is_array($destL10nRecords)) {
4270
                    foreach ($destL10nRecords as $record) {
4271
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4272
                    }
4273
                }
4274
            }
4275
            $languageSourceMap = [
4276
                $uid => $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4277
            ];
4278
            // Copy the localized records after the corresponding localizations of the destination record
4279
            foreach ($l10nRecords as $record) {
4280
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4281
                if ($localizedDestPid < 0) {
4282
                    $newUid = $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4283
                } else {
4284
                    $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

4284
                    $newUid = $this->copyRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
Loading history...
4285
                }
4286
                $languageSourceMap[$record['uid']] = $newUid;
4287
            }
4288
            $this->copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap);
4289
        }
4290
    }
4291
4292
    /**
4293
     * Remap languageSource field to uids of newly created records
4294
     *
4295
     * @param string $table Table name
4296
     * @param array $l10nRecords array of localized records from the page we're copying from (source records)
4297
     * @param array $languageSourceMap array mapping source records uids to newly copied uids
4298
     */
4299
    protected function copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap)
4300
    {
4301
        if (empty($GLOBALS['TCA'][$table]['ctrl']['translationSource']) || empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
4302
            return;
4303
        }
4304
        $translationSourceFieldName = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
4305
        $translationParentFieldName = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
4306
4307
        //We can avoid running these update queries by sorting the $l10nRecords by languageSource dependency (in copyL10nOverlayRecords)
4308
        //and first copy records depending on default record (and map the field).
4309
        foreach ($l10nRecords as $record) {
4310
            $oldSourceUid = $record[$translationSourceFieldName];
4311
            if ($oldSourceUid <= 0 && $record[$translationParentFieldName] > 0) {
4312
                //BC fix - in connected mode 'translationSource' field should not be 0
4313
                $oldSourceUid = $record[$translationParentFieldName];
4314
            }
4315
            if ($oldSourceUid > 0) {
4316
                if (empty($languageSourceMap[$oldSourceUid])) {
4317
                    // we don't have mapping information available e.g when copyRecord returned null
4318
                    continue;
4319
                }
4320
                $newFieldValue = $languageSourceMap[$oldSourceUid];
4321
                $updateFields = [
4322
                    $translationSourceFieldName => $newFieldValue
4323
                ];
4324
                GeneralUtility::makeInstance(ConnectionPool::class)
4325
                    ->getConnectionForTable($table)
4326
                    ->update($table, $updateFields, ['uid' => (int)$languageSourceMap[$record['uid']]]);
4327
                if ($this->BE_USER->workspace > 0) {
4328
                    GeneralUtility::makeInstance(ConnectionPool::class)
4329
                        ->getConnectionForTable($table)
4330
                        ->update($table, $updateFields, ['t3ver_oid' => (int)$languageSourceMap[$record['uid']], 't3ver_wsid' => $this->BE_USER->workspace]);
4331
                }
4332
            }
4333
        }
4334
    }
4335
4336
    /*********************************************
4337
     *
4338
     * Cmd: Moving, Localizing
4339
     *
4340
     ********************************************/
4341
    /**
4342
     * Moving single records
4343
     *
4344
     * @param string $table Table name to move
4345
     * @param int $uid Record uid to move
4346
     * @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
4347
     */
4348
    public function moveRecord($table, $uid, $destPid)
4349
    {
4350
        if (!$GLOBALS['TCA'][$table]) {
4351
            return;
4352
        }
4353
4354
        // In case the record to be moved turns out to be an offline version,
4355
        // we have to find the live version and work on that one (this case
4356
        // happens for pages with "branch" versioning type)
4357
        // @deprecated note: as "branch" versioning is deprecated since TYPO3 4.2, this
4358
        // functionality will be removed in TYPO3 4.7 (note by benni: a hook could replace this)
4359
        if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $uid, 'uid')) {
4360
            $uid = $lookForLiveVersion['uid'];
4361
        }
4362
        // Initialize:
4363
        $destPid = (int)$destPid;
4364
        // Get this before we change the pid (for logging)
4365
        $propArr = $this->getRecordProperties($table, $uid);
4366
        $moveRec = $this->getRecordProperties($table, $uid, true);
4367
        // This is the actual pid of the moving to destination
4368
        $resolvedPid = $this->resolvePid($table, $destPid);
4369
        // 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.
4370
        // If the record is a page, then there are two options: If the page is moved within itself, (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.
4371
        if ($table !== 'pages' || $resolvedPid == $moveRec['pid']) {
4372
            // Edit rights for the record...
4373
            $mayMoveAccess = $this->checkRecordUpdateAccess($table, $uid);
4374
        } else {
4375
            $mayMoveAccess = $this->doesRecordExist($table, $uid, 'delete');
4376
        }
4377
        // Finding out, if the record may be moved TO another place. Here we check insert-rights (non-pages = edit, pages = new), unless the pages are moved on the same pid, then edit-rights are checked
4378
        if ($table !== 'pages' || $resolvedPid != $moveRec['pid']) {
4379
            // Insert rights for the record...
4380
            $mayInsertAccess = $this->checkRecordInsertAccess($table, $resolvedPid, 4);
4381
        } else {
4382
            $mayInsertAccess = $this->checkRecordUpdateAccess($table, $uid);
4383
        }
4384
        // Checking if there is anything else disallowing moving the record by checking if editing is allowed
4385
        $fullLanguageCheckNeeded = $table !== 'pages';
4386
        $mayEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded);
4387
        // If moving is allowed, begin the processing:
4388
        if (!$mayEditAccess) {
4389
            $this->log($table, $uid, 4, 0, 1, 'Attempt to move record "%s" (%s) without having permissions to do so. [' . $this->BE_USER->errorMsg . ']', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4390
            return;
4391
        }
4392
4393
        if (!$mayMoveAccess) {
4394
            $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) without having permissions to do so.', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4395
            return;
4396
        }
4397
4398
        if (!$mayInsertAccess) {
4399
            $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) without having permissions to insert.', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4400
            return;
4401
        }
4402
4403
        $recordWasMoved = false;
4404
        // Move the record via a hook, used e.g. for versioning
4405
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
4406
            $hookObj = GeneralUtility::makeInstance($className);
4407
            if (method_exists($hookObj, 'moveRecord')) {
4408
                $hookObj->moveRecord($table, $uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, $this);
4409
            }
4410
        }
4411
        // Move the record if a hook hasn't moved it yet
4412
        if (!$recordWasMoved) {
4413
            $this->moveRecord_raw($table, $uid, $destPid);
4414
        }
4415
    }
4416
4417
    /**
4418
     * Moves a record without checking security of any sort.
4419
     * USE ONLY INTERNALLY
4420
     *
4421
     * @param string $table Table name to move
4422
     * @param int $uid Record uid to move
4423
     * @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
4424
     * @see moveRecord()
4425
     */
4426
    public function moveRecord_raw($table, $uid, $destPid)
4427
    {
4428
        $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
4429
        $origDestPid = $destPid;
4430
        // This is the actual pid of the moving to destination
4431
        $resolvedPid = $this->resolvePid($table, $destPid);
4432
        // Checking if the pid is negative, but no sorting row is defined. In that case, find the correct pid. Basically this check make the error message 4-13 meaning less... But you can always remove this check if you prefer the error instead of a no-good action (which is to move the record to its own page...)
4433
        // $destPid>=0 because we must correct pid in case of versioning "page" types.
4434
        if (($destPid < 0 && !$sortRow) || $destPid >= 0) {
4435
            $destPid = $resolvedPid;
4436
        }
4437
        // Get this before we change the pid (for logging)
4438
        $propArr = $this->getRecordProperties($table, $uid);
4439
        $moveRec = $this->getRecordProperties($table, $uid, true);
4440
        // Prepare user defined objects (if any) for hooks which extend this function:
4441
        $hookObjectsArr = [];
4442
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
4443
            $hookObjectsArr[] = GeneralUtility::makeInstance($className);
4444
        }
4445
        // Timestamp field:
4446
        $updateFields = [];
4447
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4448
            $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4449
        }
4450
4451
        // Check if this is a translation of a page, if so then it just needs to be kept "sorting" in sync
4452
        // Usually called from moveL10nOverlayRecords()
4453
        if ($table === 'pages') {
4454
            $defaultLanguagePageId = $this->getDefaultLanguagePageId((int)$uid);
4455
            if ($defaultLanguagePageId !== (int)$uid) {
4456
                $originalTranslationRecord = $this->recordInfo($table, $defaultLanguagePageId, 'pid,' . $sortRow);
4457
                $updateFields[$sortRow] = $originalTranslationRecord[$sortRow];
4458
                // Ensure that the PID is always the same as the default language page
4459
                $destPid = $originalTranslationRecord['pid'];
4460
            }
4461
        }
4462
4463
        // Insert as first element on page (where uid = $destPid)
4464
        if ($destPid >= 0) {
4465
            if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4466
                // Clear cache before moving
4467
                list($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

4467
                list($parentUid) = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4468
                $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4469
                // Setting PID
4470
                $updateFields['pid'] = $destPid;
4471
                // Table is sorted by 'sortby'
4472
                if ($sortRow && !isset($updateFields[$sortRow])) {
4473
                    $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4474
                    $updateFields[$sortRow] = $sortNumber;
4475
                }
4476
                // Check for child records that have also to be moved
4477
                $this->moveRecord_procFields($table, $uid, $destPid);
4478
                // Create query for update:
4479
                GeneralUtility::makeInstance(ConnectionPool::class)
4480
                    ->getConnectionForTable($table)
4481
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4482
                // Check for the localizations of that element
4483
                $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4484
                // Call post processing hooks:
4485
                foreach ($hookObjectsArr as $hookObj) {
4486
                    if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4487
                        $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4488
                    }
4489
                }
4490
4491
                $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields]);
4492
                if ($this->enableLogging) {
4493
                    // Logging...
4494
                    $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4495
                    if ($destPid != $propArr['pid']) {
4496
                        // Logged to old page
4497
                        $newPropArr = $this->getRecordProperties($table, $uid);
4498
                        $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4499
                        $this->log($table, $uid, 4, $destPid, 0, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, [$propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']], $propArr['pid']);
4500
                        // Logged to new page
4501
                        $this->log($table, $uid, 4, $destPid, 0, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4502
                    } else {
4503
                        // Logged to new page
4504
                        $this->log($table, $uid, 4, $destPid, 0, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4505
                    }
4506
                }
4507
                // Clear cache after moving
4508
                $this->registerRecordIdForPageCacheClearing($table, $uid);
4509
                $this->fixUniqueInPid($table, $uid);
4510
                // fixCopyAfterDuplFields
4511
                if ($origDestPid < 0) {
4512
                    $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
0 ignored issues
show
Bug introduced by
It seems like abs($origDestPid) can also be of type double; however, parameter $prevUid of TYPO3\CMS\Core\DataHandl...ixCopyAfterDuplFields() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

4512
                    $this->fixCopyAfterDuplFields($table, $uid, /** @scrutinizer ignore-type */ abs($origDestPid), 1);
Loading history...
4513
                }
4514
            } elseif ($this->enableLogging) {
4515
                $destPropArr = $this->getRecordProperties('pages', $destPid);
4516
                $this->log($table, $uid, 4, 0, 1, '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']);
4517
            }
4518
        } elseif ($sortRow) {
4519
            // Put after another record
4520
            // Table is being sorted
4521
            // Save the position to which the original record is requested to be moved
4522
            $originalRecordDestinationPid = $destPid;
4523
            $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4524
            // Setting the destPid to the new pid of the record.
4525
            $destPid = $sortInfo['pid'];
4526
            // If not an array, there was an error (which is already logged)
4527
            if (is_array($sortInfo)) {
4528
                if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4529
                    // clear cache before moving
4530
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4531
                    // We now update the pid and sortnumber (if not set for page translations)
4532
                    $updateFields['pid'] = $destPid;
4533
                    if (!isset($updateFields[$sortRow])) {
4534
                        $updateFields[$sortRow] = $sortInfo['sortNumber'];
4535
                    }
4536
                    // Check for child records that have also to be moved
4537
                    $this->moveRecord_procFields($table, $uid, $destPid);
4538
                    // Create query for update:
4539
                    GeneralUtility::makeInstance(ConnectionPool::class)
4540
                        ->getConnectionForTable($table)
4541
                        ->update($table, $updateFields, ['uid' => (int)$uid]);
4542
                    // Check for the localizations of that element
4543
                    $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4544
                    // Call post processing hooks:
4545
                    foreach ($hookObjectsArr as $hookObj) {
4546
                        if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4547
                            $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4548
                        }
4549
                    }
4550
                    $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields]);
4551
                    if ($this->enableLogging) {
4552
                        // Logging...
4553
                        $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4554
                        if ($destPid != $propArr['pid']) {
4555
                            // Logged to old page
4556
                            $newPropArr = $this->getRecordProperties($table, $uid);
4557
                            $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4558
                            $this->log($table, $uid, 4, 0, 0, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, [$propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']], $propArr['pid']);
4559
                            // Logged to old page
4560
                            $this->log($table, $uid, 4, 0, 0, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4561
                        } else {
4562
                            // Logged to old page
4563
                            $this->log($table, $uid, 4, 0, 0, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4564
                        }
4565
                    }
4566
                    // Clear cache after moving
4567
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4568
                    // fixUniqueInPid
4569
                    $this->fixUniqueInPid($table, $uid);
4570
                    // fixCopyAfterDuplFields
4571
                    if ($origDestPid < 0) {
4572
                        $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
4573
                    }
4574
                } elseif ($this->enableLogging) {
4575
                    $destPropArr = $this->getRecordProperties('pages', $destPid);
4576
                    $this->log($table, $uid, 4, 0, 1, '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']);
4577
                }
4578
            } else {
4579
                $this->log($table, $uid, 4, 0, 1, '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']);
4580
            }
4581
        }
4582
    }
4583
4584
    /**
4585
     * Walk through all fields of the moved record and look for children of e.g. the inline type.
4586
     * If child records are found, they are also move to the new $destPid.
4587
     *
4588
     * @param string $table Record Table
4589
     * @param string $uid Record UID
4590
     * @param string $destPid Position to move to
4591
     */
4592
    public function moveRecord_procFields($table, $uid, $destPid)
4593
    {
4594
        $row = BackendUtility::getRecordWSOL($table, $uid);
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...tility::getRecordWSOL(). ( Ignorable by Annotation )

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

4594
        $row = BackendUtility::getRecordWSOL($table, /** @scrutinizer ignore-type */ $uid);
Loading history...
4595
        if (is_array($row)) {
4596
            $conf = $GLOBALS['TCA'][$table]['columns'];
4597
            foreach ($row as $field => $value) {
4598
                $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4599
            }
4600
        }
4601
    }
4602
4603
    /**
4604
     * Move child records depending on the field type of the parent record.
4605
     *
4606
     * @param string $table Record Table
4607
     * @param string $uid Record UID
4608
     * @param string $destPid Position to move to
4609
     * @param string $field Record field
4610
     * @param string $value Record field value
4611
     * @param array $conf TCA configuration of current field
4612
     */
4613
    public function moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
4614
    {
4615
        if ($conf['type'] === 'inline') {
4616
            $foreign_table = $conf['foreign_table'];
4617
            $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4618
            if ($foreign_table && $moveChildrenWithParent) {
4619
                $inlineType = $this->getInlineFieldType($conf);
4620
                if ($inlineType === 'list' || $inlineType === 'field') {
4621
                    if ($table === 'pages') {
4622
                        // If the inline elements are related to a page record,
4623
                        // make sure they reside at that page and not at its parent
4624
                        $destPid = $uid;
4625
                    }
4626
                    $dbAnalysis = $this->createRelationHandlerInstance();
4627
                    $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

4627
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
4628
                }
4629
            }
4630
        }
4631
        // Move the records
4632
        if (isset($dbAnalysis)) {
4633
            // Moving records to a positive destination will insert each
4634
            // record at the beginning, thus the order is reversed here:
4635
            foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4636
                $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

4636
                $this->moveRecord($v['table'], $v['id'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4637
            }
4638
        }
4639
    }
4640
4641
    /**
4642
     * Find l10n-overlay records and perform the requested move action for these records.
4643
     *
4644
     * @param string $table Record Table
4645
     * @param string $uid Record UID
4646
     * @param string $destPid Position to move to
4647
     * @param string $originalRecordDestinationPid Position to move the original record to
4648
     */
4649
    public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4650
    {
4651
        // There's no need to perform this for non-localizable tables
4652
        if (!BackendUtility::isTableLocalizable($table)) {
4653
            return;
4654
        }
4655
4656
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4657
        $queryBuilder->getRestrictions()
4658
            ->removeAll()
4659
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4660
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
4661
4662
        $queryBuilder->select('*')
4663
            ->from($table)
4664
            ->where(
4665
                $queryBuilder->expr()->eq(
4666
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
4667
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
4668
                )
4669
            );
4670
4671
        if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4672
            $queryBuilder->andWhere(
4673
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
4674
            );
4675
        }
4676
4677
        $l10nRecords = $queryBuilder->execute()->fetchAll();
4678
        if (is_array($l10nRecords)) {
4679
            $localizedDestPids = [];
4680
            // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4681
            if ($originalRecordDestinationPid < 0) {
4682
                // Get the localized records of the record we are inserting after
4683
                $queryBuilder->setParameter('pointer', abs($originalRecordDestinationPid), \PDO::PARAM_INT);
4684
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
4685
                // Index the localized record uids by language
4686
                if (is_array($destL10nRecords)) {
4687
                    foreach ($destL10nRecords as $record) {
4688
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4689
                    }
4690
                }
4691
            }
4692
            // Move the localized records after the corresponding localizations of the destination record
4693
            foreach ($l10nRecords as $record) {
4694
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4695
                if ($localizedDestPid < 0) {
4696
                    $this->moveRecord($table, $record['uid'], $localizedDestPid);
4697
                } else {
4698
                    $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

4698
                    $this->moveRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4699
                }
4700
            }
4701
        }
4702
    }
4703
4704
    /**
4705
     * Localizes a record to another system language
4706
     *
4707
     * @param string $table Table name
4708
     * @param int $uid Record uid (to be localized)
4709
     * @param int $language Language ID (from sys_language table)
4710
     * @return int|bool The uid (int) of the new translated record or FALSE (bool) if something went wrong
4711
     */
4712
    public function localize($table, $uid, $language)
4713
    {
4714
        $newId = false;
4715
        $uid = (int)$uid;
4716
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize') !== false) {
4717
            return false;
4718
        }
4719
4720
        $this->registerNestedElementCall($table, $uid, 'localize');
4721
        if (!$GLOBALS['TCA'][$table]['ctrl']['languageField'] || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
4722
            $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table!', 1);
4723
            return false;
4724
        }
4725
        $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4726
        if (!$langRec) {
4727
            $this->newlog('Sys language UID "' . $language . '" not found valid!', 1);
4728
            return false;
4729
        }
4730
4731
        if (!$this->doesRecordExist($table, $uid, 'show')) {
4732
            $this->newlog('Attempt to localize record without permission', 1);
4733
            return false;
4734
        }
4735
4736
        // Getting workspace overlay if possible - this will localize versions in workspace if any
4737
        $row = BackendUtility::getRecordWSOL($table, $uid);
4738
        if (!is_array($row)) {
4739
            $this->newlog('Attempt to localize record that did not exist!', 1);
4740
            return false;
4741
        }
4742
4743
        // Make sure that records which are translated from another language than the default language have a correct
4744
        // localization source set themselves, before translating them to another language.
4745
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4746
            && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0) {
4747
            $localizationParentRecord = BackendUtility::getRecord(
4748
                $table,
4749
                $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4750
            );
4751
            if ((int)$localizationParentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4752
                $this->newlog('Localization failed; Source record contained a reference to an original record that is not a default record (which is strange)!', 1);
4753
                return false;
4754
            }
4755
        }
4756
4757
        // Default language records must never have a localization parent as they are the origin of any translation.
4758
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4759
            && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4760
            $this->newlog('Localization failed; Source record contained a reference to an original default record but is a default record itself (which is strange)!', 1);
4761
            return false;
4762
        }
4763
4764
        $pass = !BackendUtility::getRecordLocalization($table, $uid, $language, 'AND pid=' . (int)$row['pid']);
4765
4766
        if (!$pass) {
4767
            $this->newlog('Localization failed; There already was a localization for this language of the record!', 1);
4768
            return false;
4769
        }
4770
4771
        // Initialize:
4772
        $overrideValues = [];
4773
        $excludeFields = [];
4774
        // Set override values:
4775
        $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $langRec['uid'];
4776
        // If the translated record is a default language record, set it's uid as localization parent of the new record.
4777
        // If translating from any other language, no override is needed; we just can copy the localization parent of
4778
        // the original record (which is pointing to the correspondent default language record) to the new record.
4779
        // In copy / free mode the TransOrigPointer field is always set to 0, as no connection to the localization parent is wanted in that case.
4780
        // For pages, there is no "copy/free mode".
4781
        if (($this->useTransOrigPointerField || $table === 'pages') && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4782
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $uid;
4783
        } elseif (!$this->useTransOrigPointerField) {
4784
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = 0;
4785
        }
4786
        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
4787
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = $uid;
4788
        }
4789
        // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4790
        if (isset($GLOBALS['TCA'][$table]['ctrl']['type'])) {
4791
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4792
        }
4793
        // Set exclude Fields:
4794
        foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fCfg) {
4795
            $translateToMsg = '';
4796
            // Check if we are just prefixing:
4797
            if ($fCfg['l10n_mode'] === 'prefixLangTitle') {
4798
                if (($fCfg['config']['type'] === 'text' || $fCfg['config']['type'] === 'input') && (string)$row[$fN] !== '') {
4799
                    list($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

4799
                    list($tscPID) = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4800
                    $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
4801
                    if (!empty($TSConfig['translateToMessage'])) {
4802
                        $translateToMsg = $this->getLanguageService()->sL($TSConfig['translateToMessage']);
4803
                        $translateToMsg = @sprintf($translateToMsg, $langRec['title']);
4804
                    }
4805
4806
                    foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processTranslateToClass'] ?? [] as $className) {
4807
                        $hookObj = GeneralUtility::makeInstance($className);
4808
                        if (method_exists($hookObj, 'processTranslateTo_copyAction')) {
4809
                            $hookObj->processTranslateTo_copyAction($row[$fN], $langRec, $this);
4810
                        }
4811
                    }
4812
                    if (!empty($translateToMsg)) {
4813
                        $overrideValues[$fN] = '[' . $translateToMsg . '] ' . $row[$fN];
4814
                    } else {
4815
                        $overrideValues[$fN] = $row[$fN];
4816
                    }
4817
                }
4818
            } elseif (
4819
                ($fCfg['l10n_mode'] === 'exclude')
4820
                    && $fN != $GLOBALS['TCA'][$table]['ctrl']['languageField']
4821
                    && $fN != $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
4822
             ) {
4823
                // Otherwise, do not copy field (unless it is the language field or
4824
                // pointer to the original language)
4825
                $excludeFields[] = $fN;
4826
            }
4827
        }
4828
4829
        if ($table !== 'pages') {
4830
            // Get the uid of record after which this localized record should be inserted
4831
            $previousUid = $this->getPreviousLocalizedRecordUid($table, $uid, $row['pid'], $language);
4832
            // Execute the copy:
4833
            $newId = $this->copyRecord($table, $uid, -$previousUid, true, $overrideValues, implode(',', $excludeFields), $language);
4834
            $autoVersionNewId = $this->getAutoVersionId($table, $newId);
4835
            if ($autoVersionNewId !== null) {
4836
                $this->triggerRemapAction($table, $newId, [$this, 'placeholderShadowing'], [$table, $autoVersionNewId], true);
4837
            }
4838
        } else {
4839
            // Create new page which needs to contain the same pid as the original page
4840
            $overrideValues['pid'] = $row['pid'];
4841
            $temporaryId = StringUtility::getUniqueId('NEW');
4842
            $copyTCE = $this->getLocalTCE();
4843
            $copyTCE->start([$table => [$temporaryId => $overrideValues]], [], $this->BE_USER);
4844
            $copyTCE->process_datamap();
4845
            // Getting the new UID as if it had been copied:
4846
            $theNewSQLID = $copyTCE->substNEWwithIDs[$temporaryId];
4847
            if ($theNewSQLID) {
4848
                // If is by design that $table is used and not $table! See "l10nmgr" extension. Could be debated, but this is what I chose for this "pseudo case"
4849
                $this->copyMappingArray[$table][$uid] = $theNewSQLID;
4850
                $newId = $theNewSQLID;
4851
            }
4852
        }
4853
4854
        return $newId;
4855
    }
4856
4857
    /**
4858
     * Performs localization or synchronization of child records.
4859
     * The $command argument expects an array, but supports a string for backward-compatibility.
4860
     *
4861
     * $command = array(
4862
     *   'field' => 'tx_myfieldname',
4863
     *   'language' => 2,
4864
     *   // either the key 'action' or 'ids' must be set
4865
     *   'action' => 'synchronize', // or 'localize'
4866
     *   'ids' => array(1, 2, 3, 4) // child element ids
4867
     * );
4868
     *
4869
     * @param string $table The table of the localized parent record
4870
     * @param int $id The uid of the localized parent record
4871
     * @param array|string $command Defines the command to be performed (see example above)
4872
     */
4873
    protected function inlineLocalizeSynchronize($table, $id, $command)
4874
    {
4875
        $parentRecord = BackendUtility::getRecordWSOL($table, $id);
4876
4877
        // Backward-compatibility handling
4878
        if (!is_array($command)) {
4879
            // <field>, (localize | synchronize | <uid>):
4880
            $parts = GeneralUtility::trimExplode(',', $command);
4881
            $command = [
4882
                'field' => $parts[0],
4883
                // The previous process expected $id to point to the localized record already
4884
                'language' => (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]
4885
            ];
4886
            if (!MathUtility::canBeInterpretedAsInteger($parts[1])) {
4887
                $command['action'] = $parts[1];
4888
            } else {
4889
                $command['ids'] = [$parts[1]];
4890
            }
4891
        }
4892
4893
        // In case the parent record is the default language record, fetch the localization
4894
        if (empty($parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
4895
            // Fetch the live record
4896
            $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND pid<>-1');
4897
            if (empty($parentRecordLocalization)) {
4898
                if ($this->enableLogging) {
4899
                    $this->log($table, $id, 0, 0, 0, 'Localization for parent record ' . $table . ':' . $id . '" cannot be fetched', -1, [], $this->eventPid($table, $id, $parentRecord['pid']));
4900
                }
4901
                return;
4902
            }
4903
            $parentRecord = $parentRecordLocalization[0];
4904
            $id = $parentRecord['uid'];
4905
            // Process overlay for current selected workspace
4906
            BackendUtility::workspaceOL($table, $parentRecord);
4907
        }
4908
4909
        $field = $command['field'];
4910
        $language = $command['language'];
4911
        $action = $command['action'];
4912
        $ids = $command['ids'];
4913
4914
        if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4915
            return;
4916
        }
4917
4918
        $config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4919
        $foreignTable = $config['foreign_table'];
4920
4921
        $transOrigPointer = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
4922
        $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
4923
4924
        if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) {
4925
            return;
4926
        }
4927
4928
        $inlineSubType = $this->getInlineFieldType($config);
4929
        if ($inlineSubType === false) {
4930
            return;
4931
        }
4932
4933
        $transOrigRecord = BackendUtility::getRecordWSOL($table, $transOrigPointer);
4934
4935
        $removeArray = [];
4936
        $mmTable = $inlineSubType === 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : '';
4937
        // Fetch children from original language parent:
4938
        /** @var $dbAnalysisOriginal RelationHandler */
4939
        $dbAnalysisOriginal = $this->createRelationHandlerInstance();
4940
        $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $table, $config);
4941
        $elementsOriginal = [];
4942
        foreach ($dbAnalysisOriginal->itemArray as $item) {
4943
            $elementsOriginal[$item['id']] = $item;
4944
        }
4945
        unset($dbAnalysisOriginal);
4946
        // Fetch children from current localized parent:
4947
        /** @var $dbAnalysisCurrent RelationHandler */
4948
        $dbAnalysisCurrent = $this->createRelationHandlerInstance();
4949
        $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
4950
        // Perform synchronization: Possibly removal of already localized records:
4951
        if ($action === 'synchronize') {
4952
            foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
4953
                $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
4954
                if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
4955
                    $childTransOrigPointer = $childRecord[$childTransOrigPointerField];
4956
                    // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it:
4957
                    if (!isset($elementsOriginal[$childTransOrigPointer])) {
4958
                        unset($dbAnalysisCurrent->itemArray[$index]);
4959
                        $removeArray[$item['table']][$item['id']]['delete'] = 1;
4960
                    }
4961
                }
4962
            }
4963
        }
4964
        // Perform synchronization/localization: Possibly add unlocalized records for original language:
4965
        if ($action === 'localize' || $action === 'synchronize') {
4966
            foreach ($elementsOriginal as $originalId => $item) {
4967
                $item['id'] = $this->localize($item['table'], $item['id'], $language);
4968
                $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4969
                $dbAnalysisCurrent->itemArray[] = $item;
4970
            }
4971
        } elseif (!empty($ids)) {
4972
            foreach ($ids as $childId) {
4973
                if (!MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) {
4974
                    continue;
4975
                }
4976
                $item = $elementsOriginal[$childId];
4977
                $item['id'] = $this->localize($item['table'], $item['id'], $language);
4978
                $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4979
                $dbAnalysisCurrent->itemArray[] = $item;
4980
            }
4981
        }
4982
        // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4983
        $value = implode(',', $dbAnalysisCurrent->getValueArray());
4984
        $this->registerDBList[$table][$id][$field] = $value;
4985
        // Remove child records (if synchronization requested it):
4986
        if (is_array($removeArray) && !empty($removeArray)) {
4987
            /** @var DataHandler $tce */
4988
            $tce = GeneralUtility::makeInstance(__CLASS__);
4989
            $tce->enableLogging = $this->enableLogging;
4990
            $tce->start([], $removeArray);
4991
            $tce->process_cmdmap();
4992
            unset($tce);
4993
        }
4994
        $updateFields = [];
4995
        // Handle, reorder and store relations:
4996
        if ($inlineSubType === 'list') {
4997
            $updateFields = [$field => $value];
4998
        } elseif ($inlineSubType === 'field') {
4999
            $dbAnalysisCurrent->writeForeignField($config, $id);
5000
            $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
5001
        } elseif ($inlineSubType === 'mm') {
5002
            $dbAnalysisCurrent->writeMM($config['MM'], $id);
5003
            $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
5004
        }
5005
        // Update field referencing to child records of localized parent record:
5006
        if (!empty($updateFields)) {
5007
            $this->updateDB($table, $id, $updateFields);
5008
        }
5009
    }
5010
5011
    /*********************************************
5012
     *
5013
     * Cmd: Deleting
5014
     *
5015
     ********************************************/
5016
    /**
5017
     * Delete a single record
5018
     *
5019
     * @param string $table Table name
5020
     * @param int $id Record UID
5021
     */
5022
    public function deleteAction($table, $id)
5023
    {
5024
        $recordToDelete = BackendUtility::getRecord($table, $id);
5025
        // Record asked to be deleted was found:
5026
        if (is_array($recordToDelete)) {
5027
            $recordWasDeleted = false;
5028
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
5029
                $hookObj = GeneralUtility::makeInstance($className);
5030
                if (method_exists($hookObj, 'processCmdmap_deleteAction')) {
5031
                    $hookObj->processCmdmap_deleteAction($table, $id, $recordToDelete, $recordWasDeleted, $this);
5032
                }
5033
            }
5034
            // Delete the record if a hook hasn't deleted it yet
5035
            if (!$recordWasDeleted) {
5036
                $this->deleteEl($table, $id);
5037
            }
5038
        }
5039
    }
5040
5041
    /**
5042
     * Delete element from any table
5043
     *
5044
     * @param string $table Table name
5045
     * @param int $uid Record UID
5046
     * @param bool $noRecordCheck Flag: If $noRecordCheck is set, then the function does not check permission to delete record
5047
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
5048
     */
5049
    public function deleteEl($table, $uid, $noRecordCheck = false, $forceHardDelete = false)
5050
    {
5051
        if ($table === 'pages') {
5052
            $this->deletePages($uid, $noRecordCheck, $forceHardDelete);
5053
        } else {
5054
            $this->deleteVersionsForRecord($table, $uid, $forceHardDelete);
5055
            $this->deleteRecord($table, $uid, $noRecordCheck, $forceHardDelete);
5056
        }
5057
    }
5058
5059
    /**
5060
     * Delete versions for element from any table
5061
     *
5062
     * @param string $table Table name
5063
     * @param int $uid Record UID
5064
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
5065
     */
5066
    public function deleteVersionsForRecord($table, $uid, $forceHardDelete)
5067
    {
5068
        $versions = BackendUtility::selectVersionsOfRecord($table, $uid, 'uid,pid,t3ver_wsid,t3ver_state', $this->BE_USER->workspace ?: null);
5069
        if (is_array($versions)) {
5070
            foreach ($versions as $verRec) {
5071
                if (!$verRec['_CURRENT_VERSION']) {
5072
                    if ($table === 'pages') {
5073
                        $this->deletePages($verRec['uid'], true, $forceHardDelete);
5074
                    } else {
5075
                        $this->deleteRecord($table, $verRec['uid'], true, $forceHardDelete);
5076
                    }
5077
5078
                    // Delete move-placeholder
5079
                    $versionState = VersionState::cast($verRec['t3ver_state']);
5080
                    if ($versionState->equals(VersionState::MOVE_POINTER)) {
5081
                        $versionMovePlaceholder = BackendUtility::getMovePlaceholder($table, $uid, 'uid', $verRec['t3ver_wsid']);
5082
                        if (!empty($versionMovePlaceholder)) {
5083
                            $this->deleteEl($table, $versionMovePlaceholder['uid'], true, $forceHardDelete);
5084
                        }
5085
                    }
5086
                }
5087
            }
5088
        }
5089
    }
5090
5091
    /**
5092
     * Undelete a single record
5093
     *
5094
     * @param string $table Table name
5095
     * @param int $uid Record UID
5096
     */
5097
    public function undeleteRecord($table, $uid)
5098
    {
5099
        if ($this->isRecordUndeletable($table, $uid)) {
5100
            $this->deleteRecord($table, $uid, true, false, true);
5101
        }
5102
    }
5103
5104
    /**
5105
     * Deleting/Undeleting a record
5106
     * This function may not be used to delete pages-records unless the underlying records are already deleted
5107
     * Deletes a record regardless of versioning state (live or offline, doesn't matter, the uid decides)
5108
     * If both $noRecordCheck and $forceHardDelete are set it could even delete a "deleted"-flagged record!
5109
     *
5110
     * @param string $table Table name
5111
     * @param int $uid Record UID
5112
     * @param bool $noRecordCheck Flag: If $noRecordCheck is set, then the function does not check permission to delete record
5113
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
5114
     * @param bool $undeleteRecord If TRUE, the "deleted" flag is set to 0 again and thus, the item is undeleted.
5115
     */
5116
    public function deleteRecord($table, $uid, $noRecordCheck = false, $forceHardDelete = false, $undeleteRecord = false)
5117
    {
5118
        $uid = (int)$uid;
5119
        if (!$GLOBALS['TCA'][$table] || !$uid) {
5120
            $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions. [' . $this->BE_USER->errorMsg . ']');
5121
            return;
5122
        }
5123
5124
        // Checking if there is anything else disallowing deleting the record by checking if editing is allowed
5125
        $deletedRecord = $forceHardDelete || $undeleteRecord;
5126
        $fullLanguageAccessCheck = true;
5127
        if ($table === 'pages') {
5128
            // If this is a page translation, the full language access check should not be done
5129
            $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
5130
            if ($defaultLanguagePageId !== $uid) {
5131
                $fullLanguageAccessCheck = false;
5132
            }
5133
        }
5134
        $hasEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, $deletedRecord, $fullLanguageAccessCheck);
5135
        if (!$hasEditAccess) {
5136
            $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions');
5137
            return;
5138
        }
5139
        if (!$noRecordCheck && !$this->doesRecordExist($table, $uid, 'delete')) {
5140
            return;
5141
        }
5142
5143
        // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
5144
        list($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

5144
        list($parentUid) = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
5145
        $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
5146
        $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
5147
        $databaseErrorMessage = '';
5148
        if ($deleteField && !$forceHardDelete) {
5149
            $updateFields = [
5150
                $deleteField => $undeleteRecord ? 0 : 1
5151
            ];
5152
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
5153
                $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
5154
            }
5155
            // If the table is sorted, then the sorting number is set very high
5156
            if ($GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$undeleteRecord) {
5157
                $updateFields[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = 1000000000;
5158
            }
5159
            // before (un-)deleting this record, check for child records or references
5160
            $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
5161
            try {
5162
                GeneralUtility::makeInstance(ConnectionPool::class)
5163
                    ->getConnectionForTable($table)
5164
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
5165
                // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
5166
                if (!$undeleteRecord) {
5167
                    $this->deletedRecords[$table][] = (int)$uid;
5168
                    $this->deleteL10nOverlayRecords($table, $uid);
5169
                }
5170
            } catch (DBALException $e) {
5171
                $databaseErrorMessage = $e->getPrevious()->getMessage();
5172
            }
5173
        } else {
5174
            // Fetches all fields with flexforms and look for files to delete:
5175
            foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $cfg) {
5176
                $conf = $cfg['config'];
5177
                switch ($conf['type']) {
5178
                    case 'flex':
5179
                        $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
5180
5181
                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5182
                            ->getQueryBuilderForTable($table);
5183
                        $queryBuilder->getRestrictions()->removeAll();
5184
5185
                        $files = $queryBuilder
5186
                            ->select('*')
5187
                            ->from($table)
5188
                            ->where(
5189
                                $queryBuilder->expr()->eq(
5190
                                    'uid',
5191
                                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5192
                                )
5193
                            )
5194
                            ->execute()
5195
                            ->fetch();
5196
5197
                        $flexObj->traverseFlexFormXMLData($table, $fieldName, $files, $this, 'deleteRecord_flexFormCallBack');
5198
                        break;
5199
                }
5200
            }
5201
            // Fetches all fields that holds references to files
5202
            $fileFieldArr = $this->extFileFields($table);
5203
            if (!empty($fileFieldArr)) {
5204
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5205
                $queryBuilder->getRestrictions()->removeAll();
5206
                $result = $queryBuilder
5207
                    ->select(...$fileFieldArr)
5208
                    ->from($table)
5209
                    ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
5210
                    ->execute();
5211
                if ($row = $result->fetch()) {
5212
                    $fArray = $fileFieldArr;
5213
                    // MISSING: Support for MM file relations!
5214
                    foreach ($fArray as $theField) {
5215
                        // This deletes files that belonged to this record.
5216
                        $this->extFileFunctions($table, $theField, $row[$theField]);
5217
                    }
5218
                } else {
5219
                    $this->log($table, $uid, 3, 0, 100, 'Delete: Zero rows in result when trying to read filenames from record which should be deleted');
5220
                }
5221
            }
5222
            // Delete the hard way...:
5223
            try {
5224
                GeneralUtility::makeInstance(ConnectionPool::class)
5225
                    ->getConnectionForTable($table)
5226
                    ->delete($table, ['uid' => (int)$uid]);
5227
                $this->deletedRecords[$table][] = (int)$uid;
5228
                $this->deleteL10nOverlayRecords($table, $uid);
5229
            } catch (DBALException $e) {
5230
                $databaseErrorMessage = $e->getPrevious()->getMessage();
5231
            }
5232
        }
5233
        if ($this->enableLogging) {
5234
            // 1 means insert, 3 means delete
5235
            $state = $undeleteRecord ? 1 : 3;
5236
            if ($databaseErrorMessage === '') {
5237
                if ($forceHardDelete) {
5238
                    $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
5239
                } else {
5240
                    $message = $state === 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
5241
                }
5242
                $propArr = $this->getRecordProperties($table, $uid);
5243
                $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
5244
5245
                $this->log($table, $uid, $state, 0, 0, $message, 0, [
5246
                    $propArr['header'],
5247
                    $table . ':' . $uid,
5248
                    $pagePropArr['header'],
5249
                    $propArr['pid']
5250
                ], $propArr['event_pid']);
5251
            } else {
5252
                $this->log($table, $uid, $state, 0, 100, $databaseErrorMessage);
5253
            }
5254
        }
5255
5256
        // Add history entry
5257
        if ($undeleteRecord) {
5258
            $this->getRecordHistoryStore()->undeleteRecord($table, $uid);
5259
        } else {
5260
            $this->getRecordHistoryStore()->deleteRecord($table, $uid);
5261
        }
5262
5263
        // Update reference index:
5264
        $this->updateRefIndex($table, $uid);
5265
5266
        // We track calls to update the reference index as to avoid calling it twice
5267
        // with the same arguments. This is done because reference indexing is quite
5268
        // costly and the update reference index stack usually contain duplicates.
5269
        // NB: also filled and checked in loop below. The initialisation prevents
5270
        // running the "root" record twice if it appears in the stack twice.
5271
        $updateReferenceIndexCalls = [[$table, $uid]];
5272
5273
        // If there are entries in the updateRefIndexStack
5274
        if (is_array($this->updateRefIndexStack[$table]) && is_array($this->updateRefIndexStack[$table][$uid])) {
5275
            while ($args = array_pop($this->updateRefIndexStack[$table][$uid])) {
5276
                if (!in_array($args, $updateReferenceIndexCalls, true)) {
5277
                    // $args[0]: table, $args[1]: uid
5278
                    $this->updateRefIndex($args[0], $args[1]);
5279
                    $updateReferenceIndexCalls[] = $args;
5280
                }
5281
            }
5282
            unset($this->updateRefIndexStack[$table][$uid]);
5283
        }
5284
    }
5285
5286
    /**
5287
     * Call back function for deleting file relations for flexform fields in records which are being completely deleted.
5288
     *
5289
     * @param array $dsArr
5290
     * @param string $dataValue
5291
     * @param array $PA
5292
     * @param string $structurePath not used
5293
     * @param object $pObj not used
5294
     */
5295
    public function deleteRecord_flexFormCallBack($dsArr, $dataValue, $PA, $structurePath, $pObj)
5296
    {
5297
        // Use reference index object to find files in fields:
5298
        /** @var ReferenceIndex $refIndexObj */
5299
        $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
5300
        $refIndexObj->enableRuntimeCache();
5301
        $files = $refIndexObj->getRelations_procFiles($dataValue, $dsArr['TCEforms']['config'], $PA['uid']);
5302
        // Traverse files and delete them if the field is a regular file field (and not a file_reference field)
5303
        if (is_array($files) && $dsArr['TCEforms']['config']['internal_type'] === 'file') {
5304
            foreach ($files as $dat) {
5305
                if (@is_file($dat['ID_absFile'])) {
5306
                    $file = $this->getResourceFactory()->retrieveFileOrFolderObject($dat['ID_absFile']);
5307
                    $file->delete();
5308
                } else {
5309
                    $this->log('', 0, 3, 0, 100, 'Delete: Referenced file \'' . $dat['ID_absFile'] . '\' that was supposed to be deleted together with its record which didn\'t exist');
5310
                }
5311
            }
5312
        }
5313
    }
5314
5315
    /**
5316
     * Used to delete page because it will check for branch below pages and disallowed tables on the page as well.
5317
     *
5318
     * @param int $uid Page id
5319
     * @param bool $force If TRUE, pages are not checked for permission.
5320
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
5321
     */
5322
    public function deletePages($uid, $force = false, $forceHardDelete = false)
5323
    {
5324
        $uid = (int)$uid;
5325
        if ($uid === 0) {
5326
            if ($this->enableLogging) {
5327
                $this->log('pages', $uid, 0, 0, 2, 'Deleting all pages starting from the root-page is disabled.', -1, [], 0);
5328
            }
5329
            return;
5330
        }
5331
        // Getting list of pages to delete:
5332
        if ($force) {
5333
            // Returns the branch WITHOUT permission checks (0 secures that), so it cannot return -1
5334
            $pageIdsInBranch = $this->doesBranchExist('', $uid, 0, true);
5335
            $res = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
5336
        } else {
5337
            $res = $this->canDeletePage($uid);
5338
        }
5339
        // Perform deletion if not error:
5340
        if (is_array($res)) {
5341
            foreach ($res as $deleteId) {
5342
                $this->deleteSpecificPage($deleteId, $forceHardDelete);
5343
            }
5344
        } else {
5345
            /** @var FlashMessage $flashMessage */
5346
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

5346
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, /** @scrutinizer ignore-type */ true);
Loading history...
Bug introduced by
TYPO3\CMS\Core\Messaging\FlashMessage::ERROR of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

5346
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', /** @scrutinizer ignore-type */ FlashMessage::ERROR, true);
Loading history...
Bug introduced by
$res of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

5346
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, /** @scrutinizer ignore-type */ $res, '', FlashMessage::ERROR, true);
Loading history...
5347
            /** @var $flashMessageService FlashMessageService */
5348
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
5349
            $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
5350
            $this->newlog($res, 1);
0 ignored issues
show
Bug introduced by
$res of type array is incompatible with the type string expected by parameter $message of TYPO3\CMS\Core\DataHandling\DataHandler::newlog(). ( Ignorable by Annotation )

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

5350
            $this->newlog(/** @scrutinizer ignore-type */ $res, 1);
Loading history...
5351
        }
5352
    }
5353
5354
    /**
5355
     * Delete a page and all records on it.
5356
     *
5357
     * @param int $uid Page id
5358
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
5359
     * @access private
5360
     * @see deletePages()
5361
     */
5362
    public function deleteSpecificPage($uid, $forceHardDelete = false)
5363
    {
5364
        $uid = (int)$uid;
5365
        if ($uid) {
5366
            $tableNames = $this->compileAdminTables();
5367
            foreach ($tableNames as $table) {
5368
                if ($table !== 'pages') {
5369
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5370
                        ->getQueryBuilderForTable($table);
5371
5372
                    $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5373
5374
                    $statement = $queryBuilder
5375
                        ->select('uid')
5376
                        ->from($table)
5377
                        ->where($queryBuilder->expr()->eq(
5378
                            'pid',
5379
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5380
                        ))
5381
                        ->execute();
5382
5383
                    while ($row = $statement->fetch()) {
5384
                        $this->copyMovedRecordToNewLocation($table, $row['uid']);
5385
                        $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
5386
                        $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
5387
                    }
5388
                }
5389
            }
5390
            $this->copyMovedRecordToNewLocation('pages', $uid);
5391
            $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
5392
            $this->deleteRecord('pages', $uid, true, $forceHardDelete);
5393
        }
5394
    }
5395
5396
    /**
5397
     * Copies the move placeholder of a record to its new location (pid).
5398
     * This will create a "new" placeholder at the new location and
5399
     * a version for this new placeholder. The original move placeholder
5400
     * is then deleted because it is not needed anymore.
5401
     *
5402
     * This method is used to assure that moved records are not deleted
5403
     * when the origin page is deleted.
5404
     *
5405
     * @param string $table Record table
5406
     * @param int $uid Record uid
5407
     */
5408
    protected function copyMovedRecordToNewLocation($table, $uid)
5409
    {
5410
        if ($this->BE_USER->workspace > 0) {
5411
            $originalRecord = BackendUtility::getRecord($table, $uid);
5412
            $movePlaceholder = BackendUtility::getMovePlaceholder($table, $uid);
5413
            // Check whether target page to copied to is different to current page
5414
            // Cloning on the same page is superfluous and does not help at all
5415
            if (!empty($originalRecord) && !empty($movePlaceholder) && (int)$originalRecord['pid'] !== (int)$movePlaceholder['pid']) {
5416
                // If move placeholder exists, copy to new location
5417
                // This will create a New placeholder on the new location
5418
                // and a version for this new placeholder
5419
                $command = [
5420
                    $table => [
5421
                        $uid => [
5422
                            'copy' => '-' . $movePlaceholder['uid']
5423
                        ]
5424
                    ]
5425
                ];
5426
                /** @var DataHandler $dataHandler */
5427
                $dataHandler = GeneralUtility::makeInstance(__CLASS__);
5428
                $dataHandler->enableLogging = $this->enableLogging;
5429
                $dataHandler->neverHideAtCopy = true;
5430
                $dataHandler->start([], $command);
5431
                $dataHandler->process_cmdmap();
5432
                unset($dataHandler);
5433
5434
                // Delete move placeholder
5435
                $this->deleteRecord($table, $movePlaceholder['uid'], true, true);
5436
            }
5437
        }
5438
    }
5439
5440
    /**
5441
     * Used to evaluate if a page can be deleted
5442
     *
5443
     * @param int $uid Page id
5444
     * @return int[]|string If array: List of page uids to traverse and delete (means OK), if string: error message.
5445
     */
5446
    public function canDeletePage($uid)
5447
    {
5448
        $uid = (int)$uid;
5449
        $isTranslatedPage = null;
5450
5451
        // If we may at all delete this page
5452
        // If this is a page translation, do the check against the perms_* of the default page
5453
        // Because it is currently only deleting the translation
5454
        $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
5455
        if ($defaultLanguagePageId !== $uid) {
5456
            if ($this->doesRecordExist('pages', (int)$defaultLanguagePageId, 'delete')) {
5457
                $isTranslatedPage = true;
5458
            } else {
5459
                return 'Attempt to delete page without permissions';
5460
            }
5461
        } elseif (!$this->doesRecordExist('pages', $uid, 'delete')) {
5462
            return 'Attempt to delete page without permissions';
5463
        }
5464
5465
        $pageIdsInBranch = $this->doesBranchExist('', $uid, $this->pMap['delete'], true);
5466
5467
        if ($this->deleteTree) {
5468
            if ($pageIdsInBranch === -1) {
5469
                return 'Attempt to delete pages in branch without permissions';
5470
            }
5471
5472
            $pagesInBranch = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
5473
        } else {
5474
            if ($pageIdsInBranch === -1) {
5475
                return 'Attempt to delete page without permissions';
5476
            }
5477
            if ($pageIdsInBranch !== '') {
5478
                return 'Attempt to delete page which has subpages';
5479
            }
5480
5481
            $pagesInBranch = [$uid];
5482
        }
5483
5484
        if (!$this->checkForRecordsFromDisallowedTables($pagesInBranch)) {
5485
            return 'Attempt to delete records from disallowed tables';
5486
        }
5487
5488
        foreach ($pagesInBranch as $pageInBranch) {
5489
            if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, $isTranslatedPage ? false : true)) {
5490
                return 'Attempt to delete page which has prohibited localizations.';
5491
            }
5492
        }
5493
        return $pagesInBranch;
5494
    }
5495
5496
    /**
5497
     * Returns TRUE if record CANNOT be deleted, otherwise FALSE. Used to check before the versioning API allows a record to be marked for deletion.
5498
     *
5499
     * @param string $table Record Table
5500
     * @param int $id Record UID
5501
     * @return string Returns a string IF there is an error (error string explaining). FALSE means record can be deleted
5502
     */
5503
    public function cannotDeleteRecord($table, $id)
5504
    {
5505
        if ($table === 'pages') {
5506
            $res = $this->canDeletePage($id);
5507
            return is_array($res) ? false : $res;
5508
        }
5509
        return $this->doesRecordExist($table, $id, 'delete') ? false : 'No permission to delete record';
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->doesRecord...ssion to delete record' could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

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

Loading history...
5510
    }
5511
5512
    /**
5513
     * Determines whether a record can be undeleted.
5514
     *
5515
     * @param string $table Table name of the record
5516
     * @param int $uid uid of the record
5517
     * @return bool Whether the record can be undeleted
5518
     */
5519
    public function isRecordUndeletable($table, $uid)
5520
    {
5521
        $result = false;
5522
        $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5523
        if ($record['pid']) {
5524
            $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5525
            // The page containing the record is not deleted, thus the record can be undeleted:
5526
            if (!$page['deleted']) {
5527
                $result = true;
5528
            } else {
5529
                $this->log($table, $uid, 'isRecordUndeletable', '', 1, '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
'isRecordUndeletable' 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

5529
                $this->log($table, $uid, /** @scrutinizer ignore-type */ 'isRecordUndeletable', '', 1, 'Record cannot be undeleted since the page containing it is deleted! Undelete page "' . $page['title'] . ' (UID: ' . $page['uid'] . ')" first');
Loading history...
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

5529
                $this->log($table, $uid, 'isRecordUndeletable', /** @scrutinizer ignore-type */ '', 1, 'Record cannot be undeleted since the page containing it is deleted! Undelete page "' . $page['title'] . ' (UID: ' . $page['uid'] . ')" first');
Loading history...
5530
            }
5531
        } else {
5532
            // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5533
            $result = true;
5534
        }
5535
        return $result;
5536
    }
5537
5538
    /**
5539
     * Before a record is deleted, check if it has references such as inline type or MM references.
5540
     * If so, set these child records also to be deleted.
5541
     *
5542
     * @param string $table Record Table
5543
     * @param string $uid Record UID
5544
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5545
     * @see deleteRecord()
5546
     */
5547
    public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
5548
    {
5549
        $conf = $GLOBALS['TCA'][$table]['columns'];
5550
        $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

5550
        $row = BackendUtility::getRecord($table, /** @scrutinizer ignore-type */ $uid, '*', '', false);
Loading history...
5551
        if (empty($row)) {
5552
            return;
5553
        }
5554
        foreach ($row as $field => $value) {
5555
            $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5556
        }
5557
    }
5558
5559
    /**
5560
     * Process fields of a record to be deleted and search for special handling, like
5561
     * inline type, MM records, etc.
5562
     *
5563
     * @param string $table Record Table
5564
     * @param string $uid Record UID
5565
     * @param string $field Record field
5566
     * @param string $value Record field value
5567
     * @param array $conf TCA configuration of current field
5568
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5569
     * @see deleteRecord()
5570
     */
5571
    public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
5572
    {
5573
        if ($conf['type'] === 'inline') {
5574
            $foreign_table = $conf['foreign_table'];
5575
            if ($foreign_table) {
5576
                $inlineType = $this->getInlineFieldType($conf);
5577
                if ($inlineType === 'list' || $inlineType === 'field') {
5578
                    /** @var RelationHandler $dbAnalysis */
5579
                    $dbAnalysis = $this->createRelationHandlerInstance();
5580
                    $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

5580
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
5581
                    $dbAnalysis->undeleteRecord = true;
5582
5583
                    $enableCascadingDelete = true;
5584
                    // non type save comparison is intended!
5585
                    if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5586
                        $enableCascadingDelete = false;
5587
                    }
5588
5589
                    // Walk through the items and remove them
5590
                    foreach ($dbAnalysis->itemArray as $v) {
5591
                        if (!$undeleteRecord) {
5592
                            if ($enableCascadingDelete) {
5593
                                $this->deleteAction($v['table'], $v['id']);
5594
                            }
5595
                        } else {
5596
                            $this->undeleteRecord($v['table'], $v['id']);
5597
                        }
5598
                    }
5599
                }
5600
            }
5601
        } elseif ($this->isReferenceField($conf)) {
5602
            $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5603
            $dbAnalysis = $this->createRelationHandlerInstance();
5604
            $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5605
            foreach ($dbAnalysis->itemArray as $v) {
5606
                $this->updateRefIndexStack[$table][$uid][] = [$v['table'], $v['id']];
5607
            }
5608
        }
5609
    }
5610
5611
    /**
5612
     * Find l10n-overlay records and perform the requested delete action for these records.
5613
     *
5614
     * @param string $table Record Table
5615
     * @param string $uid Record UID
5616
     */
5617
    public function deleteL10nOverlayRecords($table, $uid)
5618
    {
5619
        // Check whether table can be localized
5620
        if (!BackendUtility::isTableLocalizable($table)) {
5621
            return;
5622
        }
5623
5624
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5625
        $queryBuilder->getRestrictions()
5626
            ->removeAll()
5627
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5628
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
5629
5630
        $queryBuilder->select('*')
5631
            ->from($table)
5632
            ->where(
5633
                $queryBuilder->expr()->eq(
5634
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5635
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5636
                )
5637
            );
5638
5639
        if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
5640
            $queryBuilder->andWhere(
5641
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
5642
            );
5643
        }
5644
5645
        $result = $queryBuilder->execute();
5646
        while ($record = $result->fetch()) {
5647
            // Ignore workspace delete placeholders. Those records have been marked for
5648
            // deletion before - deleting them again in a workspace would revert that state.
5649
            if ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5650
                BackendUtility::workspaceOL($table, $record);
5651
                if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5652
                    continue;
5653
                }
5654
            }
5655
            $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5656
        }
5657
    }
5658
5659
    /*********************************************
5660
     *
5661
     * Cmd: Versioning
5662
     *
5663
     ********************************************/
5664
    /**
5665
     * Creates a new version of a record
5666
     * (Requires support in the table)
5667
     *
5668
     * @param string $table Table name
5669
     * @param int $id Record uid to versionize
5670
     * @param string $label Version label
5671
     * @param bool $delete If TRUE, the version is created to delete the record.
5672
     * @return int|null Returns the id of the new version (if any)
5673
     * @see copyRecord()
5674
     */
5675
    public function versionizeRecord($table, $id, $label, $delete = false)
5676
    {
5677
        $id = (int)$id;
5678
        // Stop any actions if the record is marked to be deleted:
5679
        // (this can occur if IRRE elements are versionized and child elements are removed)
5680
        if ($this->isElementToBeDeleted($table, $id)) {
5681
            return null;
5682
        }
5683
        if (!$GLOBALS['TCA'][$table] || !$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] || $id <= 0) {
5684
            $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, 1);
5685
            return null;
5686
        }
5687
5688
        // Fetch record with permission check
5689
        $row = $this->recordInfoWithPermissionCheck($table, $id, 'show');
5690
5691
        // This checks if the record can be selected which is all that a copy action requires.
5692
        if ($row === false) {
5693
            $this->newlog(
5694
                'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "' . $table . ':' . $id . '"',
5695
                1
5696
            );
5697
            return null;
5698
        }
5699
5700
        // Record must be online record
5701
        if ($row['pid'] < 0) {
5702
            $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (pid=-1)!', 1);
5703
            return null;
5704
        }
5705
5706
        // Record must not be placeholder for moving.
5707
        if (VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
5708
            $this->newlog('Record cannot be versioned because it is a placeholder for a moving operation', 1);
5709
            return null;
5710
        }
5711
5712
        if ($delete && $this->cannotDeleteRecord($table, $id)) {
5713
            $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), 1);
5714
            return null;
5715
        }
5716
5717
        // Look for next version number:
5718
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5719
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5720
        $highestVerNumber = $queryBuilder
5721
            ->select('t3ver_id')
5722
            ->from($table)
5723
            ->where($queryBuilder->expr()->orX(
5724
                $queryBuilder->expr()->andX(
5725
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
5726
                    $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
5727
                ),
5728
                $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
5729
            ))
5730
            ->orderBy('t3ver_id', 'DESC')
5731
            ->setMaxResults(1)
5732
            ->execute()
5733
            ->fetchColumn();
5734
        // Look for version number of the current:
5735
        $subVer = $row['t3ver_id'] . '.' . ($highestVerNumber + 1);
5736
        // Set up the values to override when making a raw-copy:
5737
        $overrideArray = [
5738
            't3ver_id' => $highestVerNumber + 1,
5739
            't3ver_oid' => $id,
5740
            't3ver_label' => $label ?: $subVer . ' / ' . date('d-m-Y H:m:s'),
5741
            't3ver_wsid' => $this->BE_USER->workspace,
5742
            't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5743
            't3ver_count' => 0,
5744
            't3ver_stage' => 0,
5745
            't3ver_tstamp' => 0
5746
        ];
5747
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5748
            $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5749
        }
5750
        // Checking if the record already has a version in the current workspace of the backend user
5751
        if ($this->BE_USER->workspace !== 0) {
5752
            // Look for version already in workspace:
5753
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5754
        }
5755
        // Create new version of the record and return the new uid
5756
        if (empty($versionRecord['uid'])) {
5757
            // Create raw-copy and return result:
5758
            // The information of the label to be used for the workspace record
5759
            // as well as the information whether the record shall be removed
5760
            // must be forwarded (creating remove placeholders on a workspace are
5761
            // done by copying the record and override several fields).
5762
            $workspaceOptions = [
5763
                'delete' => $delete,
5764
                'label' => $label,
5765
            ];
5766
            return $this->copyRecord_raw($table, $id, -1, $overrideArray, $workspaceOptions);
5767
        }
5768
        // Reuse the existing record and return its uid
5769
        // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5770
        // did not make much sense since the information is available)
5771
        return $versionRecord['uid'];
5772
    }
5773
5774
    /**
5775
     * Swaps MM-relations for current/swap record, see version_swap()
5776
     *
5777
     * @param string $table Table for the two input records
5778
     * @param int $id Current record (about to go offline)
5779
     * @param int $swapWith Swap record (about to go online)
5780
     * @see version_swap()
5781
     */
5782
    public function version_remapMMForVersionSwap($table, $id, $swapWith)
5783
    {
5784
        // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5785
        $currentRec = BackendUtility::getRecord($table, $id);
5786
        $swapRec = BackendUtility::getRecord($table, $swapWith);
5787
        $this->version_remapMMForVersionSwap_reg = [];
5788
        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5789
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5790
            $conf = $fConf['config'];
5791
            if ($this->isReferenceField($conf)) {
5792
                $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5793
                $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5794
                if ($conf['MM']) {
5795
                    /** @var $dbAnalysis RelationHandler */
5796
                    $dbAnalysis = $this->createRelationHandlerInstance();
5797
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5798
                    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

5798
                    if (!empty($dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName))) {
Loading history...
5799
                        $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5800
                    }
5801
                    /** @var $dbAnalysis RelationHandler */
5802
                    $dbAnalysis = $this->createRelationHandlerInstance();
5803
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5804
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
5805
                        $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5806
                    }
5807
                }
5808
            } elseif ($conf['type'] === 'flex') {
5809
                // Current record
5810
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5811
                    $fConf,
5812
                    $table,
5813
                    $field,
5814
                    $currentRec
5815
                );
5816
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5817
                $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5818
                if (is_array($currentValueArray)) {
5819
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5820
                }
5821
                // Swap record
5822
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5823
                    $fConf,
5824
                    $table,
5825
                    $field,
5826
                    $swapRec
5827
                );
5828
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5829
                $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5830
                if (is_array($currentValueArray)) {
5831
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5832
                }
5833
            }
5834
        }
5835
        // Execute:
5836
        $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5837
    }
5838
5839
    /**
5840
     * Callback function for traversing the FlexForm structure in relation to ...
5841
     *
5842
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
5843
     * @param array $dsConf TCA field configuration (from Data Structure XML)
5844
     * @param string $dataValue The value of the flexForm field
5845
     * @param string $dataValue_ext1 Not used.
5846
     * @param string $dataValue_ext2 Not used.
5847
     * @param string $path Path in flexforms
5848
     * @return array Result array with key "value" containing the value of the processing.
5849
     * @see version_remapMMForVersionSwap(), checkValue_flex_procInData_travDS()
5850
     */
5851
    public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
5852
    {
5853
        // Extract parameters:
5854
        list($table, $uid, $field) = $pParams;
5855
        if ($this->isReferenceField($dsConf)) {
5856
            $allowedTables = $dsConf['type'] === 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5857
            $prependName = $dsConf['type'] === 'group' ? $dsConf['prepend_tname'] : '';
5858
            if ($dsConf['MM']) {
5859
                /** @var $dbAnalysis RelationHandler */
5860
                $dbAnalysis = $this->createRelationHandlerInstance();
5861
                $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5862
                $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5863
            }
5864
        }
5865
    }
5866
5867
    /**
5868
     * Performing the remapping operations found necessary in version_remapMMForVersionSwap()
5869
     * 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.
5870
     *
5871
     * @param string $table Table for the two input records
5872
     * @param int $id Current record (about to go offline)
5873
     * @param int $swapWith Swap record (about to go online)
5874
     * @see version_remapMMForVersionSwap()
5875
     */
5876
    public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
5877
    {
5878
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5879
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5880
                $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5881
            }
5882
        }
5883
        if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5884
            foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5885
                $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5886
            }
5887
        }
5888
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5889
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5890
                $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5891
            }
5892
        }
5893
    }
5894
5895
    /*********************************************
5896
     *
5897
     * Cmd: Helper functions
5898
     *
5899
     ********************************************/
5900
5901
    /**
5902
     * Returns an instance of DataHandler for handling local datamaps/cmdmaps
5903
     *
5904
     * @return DataHandler
5905
     */
5906
    protected function getLocalTCE()
5907
    {
5908
        $copyTCE = GeneralUtility::makeInstance(DataHandler::class);
5909
        $copyTCE->copyTree = $this->copyTree;
5910
        $copyTCE->enableLogging = $this->enableLogging;
5911
        // Copy forth the cached TSconfig
5912
        $copyTCE->cachedTSconfig = $this->cachedTSconfig;
5913
        // Transformations should NOT be carried out during copy
5914
        $copyTCE->dontProcessTransformations = true;
5915
        // make sure the isImporting flag is transferred, so all hooks know if
5916
        // the current process is an import process
5917
        $copyTCE->isImporting = $this->isImporting;
5918
        return $copyTCE;
5919
    }
5920
5921
    /**
5922
     * Processes the fields with references as registered during the copy process. This includes all FlexForm fields which had references.
5923
     */
5924
    public function remapListedDBRecords()
5925
    {
5926
        if (!empty($this->registerDBList)) {
5927
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5928
            foreach ($this->registerDBList as $table => $records) {
5929
                foreach ($records as $uid => $fields) {
5930
                    $newData = [];
5931
                    $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5932
                    $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5933
                    foreach ($fields as $fieldName => $value) {
5934
                        $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5935
                        switch ($conf['type']) {
5936
                            case 'group':
5937
                            case 'select':
5938
                                $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5939
                                if (is_array($vArray)) {
5940
                                    $newData[$fieldName] = implode(',', $vArray);
5941
                                }
5942
                                break;
5943
                            case 'flex':
5944
                                if ($value === 'FlexForm_reference') {
5945
                                    // This will fetch the new row for the element
5946
                                    $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5947
                                    if (is_array($origRecordRow)) {
5948
                                        BackendUtility::workspaceOL($table, $origRecordRow);
5949
                                        // Get current data structure and value array:
5950
                                        $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5951
                                            [ 'config' => $conf ],
5952
                                            $table,
5953
                                            $fieldName,
5954
                                            $origRecordRow
5955
                                        );
5956
                                        $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5957
                                        $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5958
                                        // Do recursive processing of the XML data:
5959
                                        $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5960
                                        // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5961
                                        if (is_array($currentValueArray['data'])) {
5962
                                            $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
0 ignored issues
show
Bug introduced by
It seems like $currentValueArray can also be of type string; however, parameter $array of TYPO3\CMS\Core\DataHandl...ckValue_flexArray2Xml() 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

5962
                                            $newData[$fieldName] = $this->checkValue_flexArray2Xml(/** @scrutinizer ignore-type */ $currentValueArray, true);
Loading history...
5963
                                        }
5964
                                    }
5965
                                }
5966
                                break;
5967
                            case 'inline':
5968
                                $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5969
                                break;
5970
                            default:
5971
                                $this->logger->debug('Field type should not appear here: ' . $conf['type']);
5972
                        }
5973
                    }
5974
                    // If any fields were changed, those fields are updated!
5975
                    if (!empty($newData)) {
5976
                        $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5977
                    }
5978
                }
5979
            }
5980
        }
5981
    }
5982
5983
    /**
5984
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
5985
     *
5986
     * @param array $pParams Set of parameters in numeric array: table, uid, field
5987
     * @param array $dsConf TCA config for field (from Data Structure of course)
5988
     * @param string $dataValue Field value (from FlexForm XML)
5989
     * @param string $dataValue_ext1 Not used
5990
     * @param string $dataValue_ext2 Not used
5991
     * @return array Array where the "value" key carries the value.
5992
     * @see checkValue_flex_procInData_travDS(), remapListedDBRecords()
5993
     */
5994
    public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
5995
    {
5996
        // Extract parameters:
5997
        list($table, $uid, $field) = $pParams;
5998
        // If references are set for this field, set flag so they can be corrected later:
5999
        if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
6000
            $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
6001
            if (is_array($vArray)) {
6002
                $dataValue = implode(',', $vArray);
6003
            }
6004
        }
6005
        // Return
6006
        return ['value' => $dataValue];
6007
    }
6008
6009
    /**
6010
     * Performs remapping of old UID values to NEW uid values for a DB reference field.
6011
     *
6012
     * @param array $conf TCA field config
6013
     * @param string $value Field value
6014
     * @param int $MM_localUid UID of local record (for MM relations - might need to change if support for FlexForms should be done!)
6015
     * @param string $table Table name
6016
     * @return array|null Returns array of items ready to implode for field content.
6017
     * @see remapListedDBRecords()
6018
     */
6019
    public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
6020
    {
6021
        // Initialize variables
6022
        // Will be set TRUE if an upgrade should be done...
6023
        $set = false;
6024
        // Allowed tables for references.
6025
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
6026
        // Table name to prepend the UID
6027
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
6028
        // Which tables that should possibly not be remapped
6029
        $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
6030
        // Convert value to list of references:
6031
        $dbAnalysis = $this->createRelationHandlerInstance();
6032
        $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && $conf['allowNonIdValues'];
6033
        $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
6034
        // Traverse those references and map IDs:
6035
        foreach ($dbAnalysis->itemArray as $k => $v) {
6036
            $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
6037
            if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
6038
                $dbAnalysis->itemArray[$k]['id'] = $mapID;
6039
                $set = true;
6040
            }
6041
        }
6042
        if (!empty($conf['MM'])) {
6043
            // Purge invalid items (live/version)
6044
            $dbAnalysis->purgeItemArray();
6045
            if ($dbAnalysis->isPurged()) {
6046
                $set = true;
6047
            }
6048
6049
            // If record has been versioned/copied in this process, handle invalid relations of the live record
6050
            $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
6051
            $originalId = 0;
6052
            if (!empty($this->copyMappingArray_merged[$table])) {
6053
                $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
6054
            }
6055
            if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
6056
                $liveRelations = $this->createRelationHandlerInstance();
6057
                $liveRelations->setWorkspaceId(0);
6058
                $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
6059
                // Purge invalid relations in the live workspace ("0")
6060
                $liveRelations->purgeItemArray(0);
6061
                if ($liveRelations->isPurged()) {
6062
                    $liveRelations->writeMM($conf['MM'], $liveId, $prependName);
6063
                }
6064
            }
6065
        }
6066
        // If a change has been done, set the new value(s)
6067
        if ($set) {
6068
            if ($conf['MM']) {
6069
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $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

6069
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, /** @scrutinizer ignore-type */ $prependName);
Loading history...
6070
            } else {
6071
                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

6071
                return $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName);
Loading history...
6072
            }
6073
        }
6074
        return null;
6075
    }
6076
6077
    /**
6078
     * Performs remapping of old UID values to NEW uid values for an inline field.
6079
     *
6080
     * @param array $conf TCA field config
6081
     * @param string $value Field value
6082
     * @param int $uid The uid of the ORIGINAL record
6083
     * @param string $table Table name
6084
     */
6085
    public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
6086
    {
6087
        $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
6088
        if ($conf['foreign_table']) {
6089
            $inlineType = $this->getInlineFieldType($conf);
6090
            if ($inlineType === 'mm') {
6091
                $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
6092
            } elseif ($inlineType !== false) {
6093
                /** @var $dbAnalysis RelationHandler */
6094
                $dbAnalysis = $this->createRelationHandlerInstance();
6095
                $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
6096
6097
                // Keep original (live) item array and update values for specific versioned records
6098
                $originalItemArray = $dbAnalysis->itemArray;
6099
                foreach ($dbAnalysis->itemArray as &$item) {
6100
                    $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
6101
                    if (!empty($versionedId)) {
6102
                        $item['id'] = $versionedId;
6103
                    }
6104
                }
6105
6106
                // Update child records if using pointer fields ('foreign_field'):
6107
                if ($inlineType === 'field') {
6108
                    $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
6109
                }
6110
                $thePidToUpdate = null;
6111
                // If the current field is set on a page record, update the pid of related child records:
6112
                if ($table === 'pages') {
6113
                    $thePidToUpdate = $theUidToUpdate;
6114
                } elseif (isset($this->registerDBPids[$table][$uid])) {
6115
                    $thePidToUpdate = $this->registerDBPids[$table][$uid];
6116
                    $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
6117
                }
6118
6119
                // Update child records if change to pid is required (only if the current record is not on a workspace):
6120
                if ($thePidToUpdate) {
6121
                    // Ensure that only the default language page is used as PID
6122
                    $thePidToUpdate = $this->getDefaultLanguagePageId($thePidToUpdate);
6123
                    // ensure, only live page ids are used as 'pid' values
6124
                    $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
6125
                    if ($liveId !== null) {
6126
                        $thePidToUpdate = $liveId;
6127
                    }
6128
                    $updateValues = ['pid' => $thePidToUpdate];
6129
                    foreach ($originalItemArray as $v) {
6130
                        if ($v['id'] && $v['table'] && BackendUtility::getLiveVersionIdOfRecord($v['table'], $v['id']) === null) {
6131
                            GeneralUtility::makeInstance(ConnectionPool::class)
6132
                                ->getConnectionForTable($v['table'])
6133
                                ->update($v['table'], $updateValues, ['uid' => (int)$v['id']]);
6134
                        }
6135
                    }
6136
                }
6137
            }
6138
        }
6139
    }
6140
6141
    /**
6142
     * Processes the $this->remapStack at the end of copying, inserting, etc. actions.
6143
     * The remapStack takes care about the correct mapping of new and old uids in case of relational data.
6144
     */
6145
    public function processRemapStack()
6146
    {
6147
        // Processes the remap stack:
6148
        if (is_array($this->remapStack)) {
6149
            $remapFlexForms = [];
6150
            $hookPayload = [];
6151
6152
            foreach ($this->remapStack as $remapAction) {
6153
                // If no position index for the arguments was set, skip this remap action:
6154
                if (!is_array($remapAction['pos'])) {
6155
                    continue;
6156
                }
6157
                // Load values from the argument array in remapAction:
6158
                $field = $remapAction['field'];
6159
                $id = $remapAction['args'][$remapAction['pos']['id']];
6160
                $rawId = $id;
6161
                $table = $remapAction['args'][$remapAction['pos']['table']];
6162
                $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
6163
                $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
6164
                $additionalData = $remapAction['additionalData'];
6165
                // The record is new and has one or more new ids (in case of versioning/workspaces):
6166
                if (strpos($id, 'NEW') !== false) {
6167
                    // Replace NEW...-ID with real uid:
6168
                    $id = $this->substNEWwithIDs[$id];
6169
                    // If the new parent record is on a non-live workspace or versionized, it has another new id:
6170
                    if (isset($this->autoVersionIdMap[$table][$id])) {
6171
                        $id = $this->autoVersionIdMap[$table][$id];
6172
                    }
6173
                    $remapAction['args'][$remapAction['pos']['id']] = $id;
6174
                }
6175
                // Replace relations to NEW...-IDs in field value (uids of child records):
6176
                if (is_array($valueArray)) {
6177
                    foreach ($valueArray as $key => $value) {
6178
                        if (strpos($value, 'NEW') !== false) {
6179
                            if (strpos($value, '_') === false) {
6180
                                $affectedTable = $tcaFieldConf['foreign_table'];
6181
                                $prependTable = false;
6182
                            } else {
6183
                                $parts = explode('_', $value);
6184
                                $value = array_pop($parts);
6185
                                $affectedTable = implode('_', $parts);
6186
                                $prependTable = true;
6187
                            }
6188
                            $value = $this->substNEWwithIDs[$value];
6189
                            // The record is new, but was also auto-versionized and has another new id:
6190
                            if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
6191
                                $value = $this->autoVersionIdMap[$affectedTable][$value];
6192
                            }
6193
                            if ($prependTable) {
6194
                                $value = $affectedTable . '_' . $value;
6195
                            }
6196
                            // Set a hint that this was a new child record:
6197
                            $this->newRelatedIDs[$affectedTable][] = $value;
6198
                            $valueArray[$key] = $value;
6199
                        }
6200
                    }
6201
                    $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
6202
                }
6203
                // Process the arguments with the defined function:
6204
                if (!empty($remapAction['func'])) {
6205
                    $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
6206
                }
6207
                // If array is returned, check for maxitems condition, if string is returned this was already done:
6208
                if (is_array($newValue)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $newValue does not seem to be defined for all execution paths leading up to this point.
Loading history...
6209
                    $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
6210
                    // The reference casting is only required if
6211
                    // checkValue_group_select_processDBdata() returns an array
6212
                    $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
6213
                }
6214
                // Update in database (list of children (csv) or number of relations (foreign_field)):
6215
                if (!empty($field)) {
6216
                    $fieldArray = [$field => $newValue];
6217
                    if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
6218
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
6219
                    }
6220
                    $this->updateDB($table, $id, $fieldArray);
6221
                } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
6222
                    // Collect data to update FlexForms
6223
                    $flexFormId = $additionalData['flexFormId'];
6224
                    $flexFormPath = $additionalData['flexFormPath'];
6225
6226
                    if (!isset($remapFlexForms[$flexFormId])) {
6227
                        $remapFlexForms[$flexFormId] = [];
6228
                    }
6229
6230
                    $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
6231
                }
6232
6233
                // Collect elements that shall trigger processDatamap_afterDatabaseOperations
6234
                if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
6235
                    $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
6236
                    if (!isset($hookPayload[$table][$rawId])) {
6237
                        $hookPayload[$table][$rawId] = [
6238
                            'status' => $hookArgs['status'],
6239
                            'fieldArray' => $hookArgs['fieldArray'],
6240
                            'hookObjects' => $hookArgs['hookObjectsArr'],
6241
                        ];
6242
                    }
6243
                    $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
6244
                }
6245
            }
6246
6247
            if ($remapFlexForms) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $remapFlexForms 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...
6248
                foreach ($remapFlexForms as $flexFormId => $modifications) {
6249
                    $this->updateFlexFormData($flexFormId, $modifications);
6250
                }
6251
            }
6252
6253
            foreach ($hookPayload as $tableName => $rawIdPayload) {
6254
                foreach ($rawIdPayload as $rawId => $payload) {
6255
                    foreach ($payload['hookObjects'] as $hookObject) {
6256
                        if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
6257
                            continue;
6258
                        }
6259
                        $hookObject->processDatamap_afterDatabaseOperations(
6260
                            $payload['status'],
6261
                            $tableName,
6262
                            $rawId,
6263
                            $payload['fieldArray'],
6264
                            $this
6265
                        );
6266
                    }
6267
                }
6268
            }
6269
        }
6270
        // Processes the remap stack actions:
6271
        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...
6272
            foreach ($this->remapStackActions as $action) {
6273
                if (isset($action['callback'], $action['arguments'])) {
6274
                    call_user_func_array($action['callback'], $action['arguments']);
6275
                }
6276
            }
6277
        }
6278
        // Processes the reference index updates of the remap stack:
6279
        foreach ($this->remapStackRefIndex as $table => $idArray) {
6280
            foreach ($idArray as $id) {
6281
                $this->updateRefIndex($table, $id);
6282
                unset($this->remapStackRefIndex[$table][$id]);
6283
            }
6284
        }
6285
        // Reset:
6286
        $this->remapStack = [];
6287
        $this->remapStackRecords = [];
6288
        $this->remapStackActions = [];
6289
        $this->remapStackRefIndex = [];
6290
    }
6291
6292
    /**
6293
     * Updates FlexForm data.
6294
     *
6295
     * @param string $flexFormId, e.g. <table>:<uid>:<field>
6296
     * @param array $modifications Modifications with paths and values (e.g. 'sDEF/lDEV/field/vDEF' => 'TYPO3')
6297
     */
6298
    protected function updateFlexFormData($flexFormId, array $modifications)
6299
    {
6300
        list($table, $uid, $field) = explode(':', $flexFormId, 3);
6301
6302
        if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
6303
            $uid = $this->substNEWwithIDs[$uid];
6304
        }
6305
6306
        $record = $this->recordInfo($table, $uid, '*');
6307
6308
        if (!$table || !$uid || !$field || !is_array($record)) {
6309
            return;
6310
        }
6311
6312
        BackendUtility::workspaceOL($table, $record);
6313
6314
        // Get current data structure and value array:
6315
        $valueStructure = GeneralUtility::xml2array($record[$field]);
6316
6317
        // Do recursive processing of the XML data:
6318
        foreach ($modifications as $path => $value) {
6319
            $valueStructure['data'] = ArrayUtility::setValueByPath(
6320
                $valueStructure['data'],
6321
                $path,
6322
                $value
6323
            );
6324
        }
6325
6326
        if (is_array($valueStructure['data'])) {
6327
            // The return value should be compiled back into XML
6328
            $values = [
6329
                $field => $this->checkValue_flexArray2Xml($valueStructure, true),
0 ignored issues
show
Bug introduced by
It seems like $valueStructure can also be of type string; however, parameter $array of TYPO3\CMS\Core\DataHandl...ckValue_flexArray2Xml() 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

6329
                $field => $this->checkValue_flexArray2Xml(/** @scrutinizer ignore-type */ $valueStructure, true),
Loading history...
6330
            ];
6331
6332
            $this->updateDB($table, $uid, $values);
6333
        }
6334
    }
6335
6336
    /**
6337
     * Triggers a remap action for a specific record.
6338
     *
6339
     * Some records are post-processed by the processRemapStack() method (e.g. IRRE children).
6340
     * This method determines whether an action/modification is executed directly to a record
6341
     * or is postponed to happen after remapping data.
6342
     *
6343
     * @param string $table Name of the table
6344
     * @param string $id Id of the record (can also be a "NEW..." string)
6345
     * @param array $callback The method to be called
6346
     * @param array $arguments The arguments to be submitted to the callback method
6347
     * @param bool $forceRemapStackActions Whether to force to use the stack
6348
     * @see processRemapStack
6349
     */
6350
    protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
6351
    {
6352
        // Check whether the affected record is marked to be remapped:
6353
        if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
6354
            call_user_func_array($callback, $arguments);
6355
        } else {
6356
            $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

6356
            $this->addRemapAction($table, /** @scrutinizer ignore-type */ $id, $callback, $arguments);
Loading history...
6357
        }
6358
    }
6359
6360
    /**
6361
     * Adds an instruction to the remap action stack (used with IRRE).
6362
     *
6363
     * @param string $table The affected table
6364
     * @param int $id The affected ID
6365
     * @param array $callback The callback information (object and method)
6366
     * @param array $arguments The arguments to be used with the callback
6367
     */
6368
    public function addRemapAction($table, $id, array $callback, array $arguments)
6369
    {
6370
        $this->remapStackActions[] = [
6371
            'affects' => [
6372
                'table' => $table,
6373
                'id' => $id
6374
            ],
6375
            'callback' => $callback,
6376
            'arguments' => $arguments
6377
        ];
6378
    }
6379
6380
    /**
6381
     * Adds a table-id-pair to the reference index remapping stack.
6382
     *
6383
     * @param string $table
6384
     * @param int $id
6385
     */
6386
    public function addRemapStackRefIndex($table, $id)
6387
    {
6388
        $this->remapStackRefIndex[$table][$id] = $id;
6389
    }
6390
6391
    /**
6392
     * If a parent record was versionized on a workspace in $this->process_datamap,
6393
     * it might be possible, that child records (e.g. on using IRRE) were affected.
6394
     * This function finds these relations and updates their uids in the $incomingFieldArray.
6395
     * The $incomingFieldArray is updated by reference!
6396
     *
6397
     * @param string $table Table name of the parent record
6398
     * @param int $id Uid of the parent record
6399
     * @param array $incomingFieldArray Reference to the incomingFieldArray of process_datamap
6400
     * @param array $registerDBList Reference to the $registerDBList array that was created/updated by versionizing calls to DataHandler in process_datamap.
6401
     */
6402
    public function getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
6403
    {
6404
        if (is_array($registerDBList[$table][$id])) {
6405
            foreach ($incomingFieldArray as $field => $value) {
6406
                $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
6407
                if ($registerDBList[$table][$id][$field] && ($foreignTable = $fieldConf['foreign_table'])) {
6408
                    $newValueArray = [];
6409
                    $origValueArray = explode(',', $value);
6410
                    // Update the uids of the copied records, but also take care about new records:
6411
                    foreach ($origValueArray as $childId) {
6412
                        $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ? $this->autoVersionIdMap[$foreignTable][$childId] : $childId;
6413
                    }
6414
                    // Set the changed value to the $incomingFieldArray
6415
                    $incomingFieldArray[$field] = implode(',', $newValueArray);
6416
                }
6417
            }
6418
            // Clean up the $registerDBList array:
6419
            unset($registerDBList[$table][$id]);
6420
            if (empty($registerDBList[$table])) {
6421
                unset($registerDBList[$table]);
6422
            }
6423
        }
6424
    }
6425
6426
    /*****************************
6427
     *
6428
     * Access control / Checking functions
6429
     *
6430
     *****************************/
6431
    /**
6432
     * Checking group modify_table access list
6433
     *
6434
     * @param string $table Table name
6435
     * @return bool Returns TRUE if the user has general access to modify the $table
6436
     */
6437
    public function checkModifyAccessList($table)
6438
    {
6439
        $res = $this->admin || !$this->tableAdminOnly($table) && GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table);
6440
        // Hook 'checkModifyAccessList': Post-processing of the state of access
6441
        foreach ($this->getCheckModifyAccessListHookObjects() as $hookObject) {
6442
            /** @var $hookObject DataHandlerCheckModifyAccessListHookInterface */
6443
            $hookObject->checkModifyAccessList($res, $table, $this);
6444
        }
6445
        return $res;
6446
    }
6447
6448
    /**
6449
     * Checking if a record with uid $id from $table is in the BE_USERS webmounts which is required for editing etc.
6450
     *
6451
     * @param string $table Table name
6452
     * @param int $id UID of record
6453
     * @return bool Returns TRUE if OK. Cached results.
6454
     */
6455
    public function isRecordInWebMount($table, $id)
6456
    {
6457
        if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
6458
            $recP = $this->getRecordProperties($table, $id);
6459
            $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
6460
        }
6461
        return $this->isRecordInWebMount_Cache[$table . ':' . $id];
6462
    }
6463
6464
    /**
6465
     * Checks if the input page ID is in the BE_USER webmounts
6466
     *
6467
     * @param int $pid Page ID to check
6468
     * @return bool TRUE if OK. Cached results.
6469
     */
6470
    public function isInWebMount($pid)
6471
    {
6472
        if (!isset($this->isInWebMount_Cache[$pid])) {
6473
            $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
6474
        }
6475
        return $this->isInWebMount_Cache[$pid];
6476
    }
6477
6478
    /**
6479
     * Checks if user may update a record with uid=$id from $table
6480
     *
6481
     * @param string $table Record table
6482
     * @param int $id Record UID
6483
     * @param array|bool $data Record data
6484
     * @param array $hookObjectsArr Hook objects
6485
     * @return bool Returns TRUE if the user may update the record given by $table and $id
6486
     */
6487
    public function checkRecordUpdateAccess($table, $id, $data = false, $hookObjectsArr = null)
6488
    {
6489
        $res = null;
6490
        if (is_array($hookObjectsArr)) {
6491
            foreach ($hookObjectsArr as $hookObj) {
6492
                if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
6493
                    $res = $hookObj->checkRecordUpdateAccess($table, $id, $data, $res, $this);
6494
                }
6495
            }
6496
            if (isset($res)) {
6497
                return (bool)$res;
6498
            }
6499
        }
6500
        $res = false;
6501
6502
        if ($GLOBALS['TCA'][$table] && (int)$id > 0) {
6503
            // If information is cached, return it
6504
            if (isset($this->recUpdateAccessCache[$table][$id])) {
6505
                return $this->recUpdateAccessCache[$table][$id];
6506
            }
6507
            // permissions check for page translations need to be done on the parent page
6508
            if ($table === 'pages') {
6509
                $defaultLanguagePageId = $this->getDefaultLanguagePageId($id);
6510
                $res = $this->doesRecordExist($table, $defaultLanguagePageId, 'edit');
6511
            } else {
6512
                $res = $this->doesRecordExist($table, $id, 'edit');
6513
            }
6514
            // Cache the result
6515
            $this->recUpdateAccessCache[$table][$id] = $res;
6516
        }
6517
        return $res;
6518
    }
6519
6520
    /**
6521
     * Checks if user may insert a record from $insertTable on $pid
6522
     * Does not check for workspace, use BE_USER->workspaceAllowLiveRecordsInPID for this in addition to this function call.
6523
     *
6524
     * @param string $insertTable Tablename to check
6525
     * @param int $pid Integer PID
6526
     * @param int $action For logging: Action number.
6527
     * @return bool Returns TRUE if the user may insert a record from table $insertTable on page $pid
6528
     */
6529
    public function checkRecordInsertAccess($insertTable, $pid, $action = 1)
6530
    {
6531
        $pid = (int)$pid;
6532
        if ($pid < 0) {
6533
            return false;
6534
        }
6535
        // If information is cached, return it
6536
        if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
6537
            return $this->recInsertAccessCache[$insertTable][$pid];
6538
        }
6539
6540
        $res = false;
6541
        if ($insertTable === 'pages') {
6542
            $perms = $this->pMap['new'];
6543
        } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
6544
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6545
            $perms = $this->pMap['edit'];
6546
        } else {
6547
            $perms = $this->pMap['editcontent'];
6548
        }
6549
        $pageExists = (bool)$this->doesRecordExist('pages', $pid, $perms);
6550
        // 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
6551
        if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
6552
            // Check permissions
6553
            if ($this->isTableAllowedForThisPage($pid, $insertTable)) {
6554
                $res = true;
6555
                // Cache the result
6556
                $this->recInsertAccessCache[$insertTable][$pid] = $res;
6557
            } elseif ($this->enableLogging) {
6558
                $propArr = $this->getRecordProperties('pages', $pid);
6559
                $this->log($insertTable, $pid, $action, 0, 1, 'Attempt to insert record on page \'%s\' (%s) where this table, %s, is not allowed', 11, [$propArr['header'], $pid, $insertTable], $propArr['event_pid']);
6560
            }
6561
        } elseif ($this->enableLogging) {
6562
            $propArr = $this->getRecordProperties('pages', $pid);
6563
            $this->log($insertTable, $pid, $action, 0, 1, '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']);
6564
        }
6565
        return $res;
6566
    }
6567
6568
    /**
6569
     * 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.
6570
     *
6571
     * @param int $page_uid Page id for which to check, including 0 (zero) if checking for page tree root.
6572
     * @param string $checkTable Table name to check
6573
     * @return bool TRUE if OK
6574
     */
6575
    public function isTableAllowedForThisPage($page_uid, $checkTable)
6576
    {
6577
        $page_uid = (int)$page_uid;
6578
        $rootLevelSetting = (int)$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'];
6579
        // 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.
6580
        if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$page_uid)) {
6581
            return false;
6582
        }
6583
        $allowed = false;
6584
        // Check root-level
6585
        if (!$page_uid) {
6586
            if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
6587
                $allowed = true;
6588
            }
6589
        } else {
6590
            // Check non-root-level
6591
            $doktype = $this->pageInfo($page_uid, 'doktype');
6592
            $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6593
            $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6594
            // If all tables or the table is listed as an allowed type, return TRUE
6595
            if (strpos($allowedTableList, '*') !== false || in_array($checkTable, $allowedArray, true)) {
6596
                $allowed = true;
6597
            }
6598
        }
6599
        return $allowed;
6600
    }
6601
6602
    /**
6603
     * Checks if record can be selected based on given permission criteria
6604
     *
6605
     * @param string $table Record table name
6606
     * @param int $id Record UID
6607
     * @param int|string $perms Permission restrictions to observe: Either an integer that will be bitwise AND'ed or a string, which points to a key in the ->pMap array
6608
     * @return bool Returns TRUE if the record given by $table, $id and $perms can be selected
6609
     *
6610
     * @throws \RuntimeException
6611
     */
6612
    public function doesRecordExist($table, $id, $perms)
6613
    {
6614
        return $this->recordInfoWithPermissionCheck($table, $id, $perms, 'uid, pid') !== false;
6615
    }
6616
6617
    /**
6618
     * Looks up a page based on permissions.
6619
     *
6620
     * @param int $id Page id
6621
     * @param int $perms Permission integer
6622
     * @param array $columns Columns to select
6623
     * @return bool|array
6624
     * @access private
6625
     * @see doesRecordExist()
6626
     */
6627
    protected function doesRecordExist_pageLookUp($id, $perms, $columns = ['uid'])
6628
    {
6629
        $cacheId = md5('doesRecordExist_pageLookUp' . '_' . $id . '_' . $perms . '_' . implode(
6630
            '_',
6631
                $columns
6632
        ) . '_' . (string)$this->admin);
6633
6634
        // If result is cached, return it
6635
        $cachedResult = $this->runtimeCache->get($cacheId);
6636
        if (!empty($cachedResult)) {
6637
            return $cachedResult;
6638
        }
6639
6640
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6641
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6642
        $queryBuilder
6643
            ->select(...$columns)
6644
            ->from('pages')
6645
            ->where($queryBuilder->expr()->eq(
6646
                'uid',
6647
                $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
6648
            ));
6649
        if ($perms && !$this->admin) {
6650
            $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($perms));
6651
        }
6652
        if (!$this->admin && $GLOBALS['TCA']['pages']['ctrl']['editlock'] &&
6653
            $perms & Permission::PAGE_EDIT + Permission::PAGE_DELETE + Permission::CONTENT_EDIT
6654
        ) {
6655
            $queryBuilder->andWhere($queryBuilder->expr()->eq(
6656
                $GLOBALS['TCA']['pages']['ctrl']['editlock'],
6657
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
6658
            ));
6659
        }
6660
6661
        $row = $queryBuilder->execute()->fetch();
6662
        $this->runtimeCache->set($cacheId, $row);
6663
6664
        return $row;
6665
    }
6666
6667
    /**
6668
     * Checks if a whole branch of pages exists
6669
     *
6670
     * Tests the branch under $pid like doesRecordExist(), but it doesn't test the page with $pid as uid - use doesRecordExist() for this purpose.
6671
     * 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
6672
     *
6673
     * @param string $inList List of page uids, this is added to and returned in the end
6674
     * @param int $pid Page ID to select subpages from.
6675
     * @param int $perms Perms integer to check each page record for.
6676
     * @param bool $recurse Recursion flag: If set, it will go out through the branch.
6677
     * @return string|int List of page IDs in branch, if there are subpages, empty string if there are none or -1 if no permission
6678
     */
6679
    public function doesBranchExist($inList, $pid, $perms, $recurse)
6680
    {
6681
        $pid = (int)$pid;
6682
        $perms = (int)$perms;
6683
        if ($pid >= 0) {
6684
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6685
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6686
            $result = $queryBuilder
6687
                ->select('uid', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody')
6688
                ->from('pages')
6689
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
6690
                ->orderBy('sorting')
6691
                ->execute();
6692
            while ($row = $result->fetch()) {
6693
                // IF admin, then it's OK
6694
                if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $perms)) {
6695
                    $inList .= $row['uid'] . ',';
6696
                    if ($recurse) {
6697
                        // Follow the subpages recursively...
6698
                        $inList = $this->doesBranchExist($inList, $row['uid'], $perms, $recurse);
6699
                        if ($inList === -1) {
6700
                            return -1;
6701
                        }
6702
                    }
6703
                } else {
6704
                    // No permissions
6705
                    return -1;
6706
                }
6707
            }
6708
        }
6709
        return $inList;
6710
    }
6711
6712
    /**
6713
     * Checks if the $table is readOnly
6714
     *
6715
     * @param string $table Table name
6716
     * @return bool TRUE, if readonly
6717
     */
6718
    public function tableReadOnly($table)
6719
    {
6720
        // Returns TRUE if table is readonly
6721
        return (bool)$GLOBALS['TCA'][$table]['ctrl']['readOnly'];
6722
    }
6723
6724
    /**
6725
     * Checks if the $table is only editable by admin-users
6726
     *
6727
     * @param string $table Table name
6728
     * @return bool TRUE, if readonly
6729
     */
6730
    public function tableAdminOnly($table)
6731
    {
6732
        // Returns TRUE if table is admin-only
6733
        return (bool)$GLOBALS['TCA'][$table]['ctrl']['adminOnly'];
6734
    }
6735
6736
    /**
6737
     * Checks if page $id is a uid in the rootline of page id $destinationId
6738
     * Used when moving a page
6739
     *
6740
     * @param int $destinationId Destination Page ID to test
6741
     * @param int $id Page ID to test for presence inside Destination
6742
     * @return bool Returns FALSE if ID is inside destination (including equal to)
6743
     */
6744
    public function destNotInsideSelf($destinationId, $id)
6745
    {
6746
        $loopCheck = 100;
6747
        $destinationId = (int)$destinationId;
6748
        $id = (int)$id;
6749
        if ($destinationId === $id) {
6750
            return false;
6751
        }
6752
        while ($destinationId !== 0 && $loopCheck > 0) {
6753
            $loopCheck--;
6754
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6755
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6756
            $result = $queryBuilder
6757
                ->select('pid', 'uid', 't3ver_oid', 't3ver_wsid')
6758
                ->from('pages')
6759
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($destinationId, \PDO::PARAM_INT)))
6760
                ->execute();
6761
            if ($row = $result->fetch()) {
6762
                BackendUtility::fixVersioningPid('pages', $row);
6763
                if ($row['pid'] == $id) {
6764
                    return false;
6765
                }
6766
                $destinationId = (int)$row['pid'];
6767
            } else {
6768
                return false;
6769
            }
6770
        }
6771
        return true;
6772
    }
6773
6774
    /**
6775
     * 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
6776
     * Will also generate this list for admin-users so they must be check for before calling the function
6777
     *
6778
     * @return array Array of [table]-[field] pairs to exclude from editing.
6779
     */
6780
    public function getExcludeListArray()
6781
    {
6782
        $list = [];
6783
        $nonExcludeFieldsArray = array_flip(GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
6784
        foreach ($GLOBALS['TCA'] as $table => $tableConfiguration) {
6785
            if (isset($tableConfiguration['columns'])) {
6786
                foreach ($tableConfiguration['columns'] as $field => $config) {
6787
                    if ($config['exclude'] && !isset($nonExcludeFieldsArray[$table . ':' . $field])) {
6788
                        $list[] = $table . '-' . $field;
6789
                    }
6790
                }
6791
            }
6792
        }
6793
        return $list;
6794
    }
6795
6796
    /**
6797
     * Checks if there are records on a page from tables that are not allowed
6798
     *
6799
     * @param int $page_uid Page ID
6800
     * @param int $doktype Page doktype
6801
     * @return bool|array Returns a list of the tables that are 'present' on the page but not allowed with the page_uid/doktype
6802
     */
6803
    public function doesPageHaveUnallowedTables($page_uid, $doktype)
6804
    {
6805
        $page_uid = (int)$page_uid;
6806
        if (!$page_uid) {
6807
            // Not a number. Probably a new page
6808
            return false;
6809
        }
6810
        $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6811
        // If all tables are allowed, return early
6812
        if (strpos($allowedTableList, '*') !== false) {
6813
            return false;
6814
        }
6815
        $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6816
        $tableList = [];
6817
        $allTableNames = $this->compileAdminTables();
6818
        foreach ($allTableNames as $table) {
6819
            // If the table is not in the allowed list, check if there are records...
6820
            if (in_array($table, $allowedArray, true)) {
6821
                continue;
6822
            }
6823
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6824
            $queryBuilder->getRestrictions()->removeAll();
6825
            $count = $queryBuilder
6826
                ->count('uid')
6827
                ->from($table)
6828
                ->where($queryBuilder->expr()->eq(
6829
                    'pid',
6830
                    $queryBuilder->createNamedParameter($page_uid, \PDO::PARAM_INT)
6831
                ))
6832
                ->execute()
6833
                ->fetchColumn(0);
6834
            if ($count) {
6835
                $tableList[] = $table;
6836
            }
6837
        }
6838
        return implode(',', $tableList);
6839
    }
6840
6841
    /*****************************
6842
     *
6843
     * Information lookup
6844
     *
6845
     *****************************/
6846
    /**
6847
     * Returns the value of the $field from page $id
6848
     * 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!
6849
     *
6850
     * @param int $id Page uid
6851
     * @param string $field Field name for which to return value
6852
     * @return string Value of the field. Result is cached in $this->pageCache[$id][$field] and returned from there next time!
6853
     */
6854
    public function pageInfo($id, $field)
6855
    {
6856
        if (!isset($this->pageCache[$id])) {
6857
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6858
            $queryBuilder->getRestrictions()->removeAll();
6859
            $row = $queryBuilder
6860
                ->select('*')
6861
                ->from('pages')
6862
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6863
                ->execute()
6864
                ->fetch();
6865
            if ($row) {
6866
                $this->pageCache[$id] = $row;
6867
            }
6868
        }
6869
        return $this->pageCache[$id][$field];
6870
    }
6871
6872
    /**
6873
     * Returns the row of a record given by $table and $id and $fieldList (list of fields, may be '*')
6874
     * NOTICE: No check for deleted or access!
6875
     *
6876
     * @param string $table Table name
6877
     * @param int $id UID of the record from $table
6878
     * @param string $fieldList Field list for the SELECT query, eg. "*" or "uid,pid,...
6879
     * @return array|null Returns the selected record on success, otherwise NULL.
6880
     */
6881
    public function recordInfo($table, $id, $fieldList)
6882
    {
6883
        // Skip, if searching for NEW records or there's no TCA table definition
6884
        if ((int)$id === 0 || !isset($GLOBALS['TCA'][$table])) {
6885
            return null;
6886
        }
6887
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6888
        $queryBuilder->getRestrictions()->removeAll();
6889
        $result = $queryBuilder
6890
            ->select(...GeneralUtility::trimExplode(',', $fieldList))
6891
            ->from($table)
6892
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6893
            ->execute()
6894
            ->fetch();
6895
        return $result ?: null;
6896
    }
6897
6898
    /**
6899
     * Checks if record exists with and without permission check and returns that row
6900
     *
6901
     * @param string $table Record table name
6902
     * @param int $id Record UID
6903
     * @param int|string $perms Permission restrictions to observe: Either an integer that will be bitwise AND'ed or a string, which points to a key in the ->pMap array
6904
     * @param string $fieldList - fields - default is '*'
6905
     * @throws \RuntimeException
6906
     * @return array|bool Row if exists and accessible, false otherwise
6907
     */
6908
    protected function recordInfoWithPermissionCheck(string $table, int $id, $perms, string $fieldList = '*')
6909
    {
6910
        if ($this->bypassAccessCheckForRecords) {
6911
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6912
6913
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6914
            $queryBuilder->getRestrictions()->removeAll();
6915
6916
            $record = $queryBuilder->select(...$columns)
6917
                ->from($table)
6918
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6919
                ->execute()
6920
                ->fetch();
6921
6922
            return $record ?: false;
6923
        }
6924
        // Processing the incoming $perms (from possible string to integer that can be AND'ed)
6925
        if (!MathUtility::canBeInterpretedAsInteger($perms)) {
6926
            if ($table !== 'pages') {
6927
                switch ($perms) {
6928
                    case 'edit':
6929
6930
                    case 'delete':
6931
6932
                    case 'new':
6933
                        // This holds it all in case the record is not page!!
6934
                        if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
6935
                            $perms = 'edit';
6936
                        } else {
6937
                            $perms = 'editcontent';
6938
                        }
6939
                        break;
6940
                }
6941
            }
6942
            $perms = (int)$this->pMap[$perms];
6943
        } else {
6944
            $perms = (int)$perms;
6945
        }
6946
        if (!$perms) {
6947
            throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
6948
        }
6949
        // For all tables: Check if record exists:
6950
        $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
6951
        if (is_array($GLOBALS['TCA'][$table]) && $id > 0 && ($this->admin || $isWebMountRestrictionIgnored || $this->isRecordInWebMount($table, $id))) {
6952
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6953
            if ($table !== 'pages') {
6954
                // Find record without checking page
6955
                // @todo: This should probably check for editlock
6956
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6957
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6958
                $output = $queryBuilder
6959
                    ->select(...$columns)
6960
                    ->from($table)
6961
                    ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6962
                    ->execute()
6963
                    ->fetch();
6964
                BackendUtility::fixVersioningPid($table, $output, true);
6965
                // If record found, check page as well:
6966
                if (is_array($output)) {
6967
                    // Looking up the page for record:
6968
                    $pageRec = $this->doesRecordExist_pageLookUp($output['pid'], $perms);
6969
                    // 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):
6970
                    $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
6971
                    if (is_array($pageRec) || !$output['pid'] && ($this->admin || $isRootLevelRestrictionIgnored)) {
6972
                        return $output;
6973
                    }
6974
                }
6975
                return false;
6976
            }
6977
            return $this->doesRecordExist_pageLookUp($id, $perms, $columns);
6978
        }
6979
        return false;
6980
    }
6981
6982
    /**
6983
     * Returns an array with record properties, like header and pid
6984
     * No check for deleted or access is done!
6985
     * For versionized records, pid is resolved to its live versions pid.
6986
     * Used for logging
6987
     *
6988
     * @param string $table Table name
6989
     * @param int $id Uid of record
6990
     * @param bool $noWSOL If set, no workspace overlay is performed
6991
     * @return array Properties of record
6992
     */
6993
    public function getRecordProperties($table, $id, $noWSOL = false)
6994
    {
6995
        $row = $table === 'pages' && !$id ? ['title' => '[root-level]', 'uid' => 0, 'pid' => 0] : $this->recordInfo($table, $id, '*');
6996
        if (!$noWSOL) {
6997
            BackendUtility::workspaceOL($table, $row);
6998
        }
6999
        return $this->getRecordPropertiesFromRow($table, $row);
7000
    }
7001
7002
    /**
7003
     * Returns an array with record properties, like header and pid, based on the row
7004
     *
7005
     * @param string $table Table name
7006
     * @param array $row Input row
7007
     * @return array|null Output array
7008
     */
7009
    public function getRecordPropertiesFromRow($table, $row)
7010
    {
7011
        if ($GLOBALS['TCA'][$table]) {
7012
            BackendUtility::fixVersioningPid($table, $row);
7013
            return [
7014
                'header' => BackendUtility::getRecordTitle($table, $row),
7015
                'pid' => $row['pid'],
7016
                'event_pid' => $this->eventPid($table, $row['_ORIG_pid'] ?? $row['uid'], $row['pid']),
7017
                't3ver_state' => $GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? $row['t3ver_state'] : '',
7018
                '_ORIG_pid' => $row['_ORIG_pid']
7019
            ];
7020
        }
7021
        return null;
7022
    }
7023
7024
    /**
7025
     * @param string $table
7026
     * @param int $uid
7027
     * @param int $pid
7028
     * @return int
7029
     */
7030
    public function eventPid($table, $uid, $pid)
7031
    {
7032
        return $table === 'pages' ? $uid : $pid;
7033
    }
7034
7035
    /*********************************************
7036
     *
7037
     * Storing data to Database Layer
7038
     *
7039
     ********************************************/
7040
    /**
7041
     * Update database record
7042
     * Does not check permissions but expects them to be verified on beforehand
7043
     *
7044
     * @param string $table Record table name
7045
     * @param int $id Record uid
7046
     * @param array $fieldArray Array of field=>value pairs to insert. FIELDS MUST MATCH the database FIELDS. No check is done.
7047
     */
7048
    public function updateDB($table, $id, $fieldArray)
7049
    {
7050
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && (int)$id) {
7051
            // Do NOT update the UID field, ever!
7052
            unset($fieldArray['uid']);
7053
            if (!empty($fieldArray)) {
7054
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
7055
7056
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7057
7058
                $types = [];
7059
                $platform = $connection->getDatabasePlatform();
7060
                if ($platform instanceof SQLServerPlatform) {
7061
                    // mssql needs to set proper PARAM_LOB and others to update fields
7062
                    $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
7063
                    foreach ($fieldArray as $columnName => $columnValue) {
7064
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
7065
                    }
7066
                }
7067
7068
                // Execute the UPDATE query:
7069
                $updateErrorMessage = '';
7070
                try {
7071
                    $connection->update($table, $fieldArray, ['uid' => (int)$id], $types);
7072
                } catch (DBALException $e) {
7073
                    $updateErrorMessage = $e->getPrevious()->getMessage();
7074
                }
7075
                // If succeeds, do...:
7076
                if ($updateErrorMessage === '') {
7077
                    // Update reference index:
7078
                    $this->updateRefIndex($table, $id);
7079
                    // Set History data
7080
                    $historyEntryId = 0;
7081
                    if (isset($this->historyRecords[$table . ':' . $id])) {
7082
                        $historyEntryId = $this->getRecordHistoryStore()->modifyRecord($table, $id, $this->historyRecords[$table . ':' . $id]);
7083
                    }
7084
                    if ($this->enableLogging) {
7085
                        if ($this->checkStoredRecords) {
7086
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 2);
7087
                        } else {
7088
                            $newRow = $fieldArray;
7089
                            $newRow['uid'] = $id;
7090
                        }
7091
                        // Set log entry:
7092
                        $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7093
                        $this->log($table, $id, 2, $propArr['pid'], 0, 'Record \'%s\' (%s) was updated.' . ($propArr['_ORIG_pid'] == -1 ? ' (Offline version).' : ' (Online).'), 10, [$propArr['header'], $table . ':' . $id, 'history' => $historyEntryId], $propArr['event_pid']);
7094
                    }
7095
                    // Clear cache for relevant pages:
7096
                    $this->registerRecordIdForPageCacheClearing($table, $id);
7097
                    // Unset the pageCache for the id if table was page.
7098
                    if ($table === 'pages') {
7099
                        unset($this->pageCache[$id]);
7100
                    }
7101
                } else {
7102
                    $this->log($table, $id, 2, 0, 2, 'SQL error: \'%s\' (%s)', 12, [$updateErrorMessage, $table . ':' . $id]);
7103
                }
7104
            }
7105
        }
7106
    }
7107
7108
    /**
7109
     * Insert into database
7110
     * Does not check permissions but expects them to be verified on beforehand
7111
     *
7112
     * @param string $table Record table name
7113
     * @param string $id "NEW...." uid string
7114
     * @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!
7115
     * @param bool $newVersion Set to TRUE if new version is created.
7116
     * @param int $suggestedUid Suggested UID value for the inserted record. See the array $this->suggestedInsertUids; Admin-only feature
7117
     * @param bool $dontSetNewIdIndex If TRUE, the ->substNEWwithIDs array is not updated. Only useful in very rare circumstances!
7118
     * @return int|null Returns ID on success.
7119
     */
7120
    public function insertDB($table, $id, $fieldArray, $newVersion = false, $suggestedUid = 0, $dontSetNewIdIndex = false)
7121
    {
7122
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && isset($fieldArray['pid'])) {
7123
            // Do NOT insert the UID field, ever!
7124
            unset($fieldArray['uid']);
7125
            if (!empty($fieldArray)) {
7126
                // Check for "suggestedUid".
7127
                // This feature is used by the import functionality to force a new record to have a certain UID value.
7128
                // This is only recommended for use when the destination server is a passive mirror of another server.
7129
                // As a security measure this feature is available only for Admin Users (for now)
7130
                $suggestedUid = (int)$suggestedUid;
7131
                if ($this->BE_USER->isAdmin() && $suggestedUid && $this->suggestedInsertUids[$table . ':' . $suggestedUid]) {
7132
                    // When the value of ->suggestedInsertUids[...] is "DELETE" it will try to remove the previous record
7133
                    if ($this->suggestedInsertUids[$table . ':' . $suggestedUid] === 'DELETE') {
7134
                        // DELETE:
7135
                        GeneralUtility::makeInstance(ConnectionPool::class)
7136
                            ->getConnectionForTable($table)
7137
                            ->delete($table, ['uid' => (int)$suggestedUid]);
7138
                    }
7139
                    $fieldArray['uid'] = $suggestedUid;
7140
                }
7141
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
7142
                $typeArray = [];
7143
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])
7144
                    && array_key_exists($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
7145
                ) {
7146
                    $typeArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = Connection::PARAM_LOB;
7147
                }
7148
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7149
                $insertErrorMessage = '';
7150
                try {
7151
                    // Execute the INSERT query:
7152
                    $connection->insert(
7153
                        $table,
7154
                        $fieldArray,
7155
                        $typeArray
7156
                    );
7157
                } catch (DBALException $e) {
7158
                    $insertErrorMessage = $e->getPrevious()->getMessage();
7159
                }
7160
                // If succees, do...:
7161
                if ($insertErrorMessage === '') {
7162
                    // Set mapping for NEW... -> real uid:
7163
                    // the NEW_id now holds the 'NEW....' -id
7164
                    $NEW_id = $id;
7165
                    $id = $this->postProcessDatabaseInsert($connection, $table, $suggestedUid);
7166
7167
                    if (!$dontSetNewIdIndex) {
7168
                        $this->substNEWwithIDs[$NEW_id] = $id;
7169
                        $this->substNEWwithIDs_table[$NEW_id] = $table;
7170
                    }
7171
                    $newRow = [];
7172
                    if ($this->enableLogging) {
7173
                        // Checking the record is properly saved if configured
7174
                        if ($this->checkStoredRecords) {
7175
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 1);
7176
                        } else {
7177
                            $newRow = $fieldArray;
7178
                            $newRow['uid'] = $id;
7179
                        }
7180
                    }
7181
                    // Update reference index:
7182
                    $this->updateRefIndex($table, $id);
7183
7184
                    // Store in history
7185
                    $this->getRecordHistoryStore()->addRecord($table, $id, $newRow);
7186
7187
                    if ($newVersion) {
7188
                        if ($this->enableLogging) {
7189
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7190
                            $this->log($table, $id, 1, 0, 0, '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);
7191
                        }
7192
                    } else {
7193
                        if ($this->enableLogging) {
7194
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7195
                            $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
7196
                            $this->log($table, $id, 1, 0, 0, 'Record \'%s\' (%s) was inserted on page \'%s\' (%s)', 10, [$propArr['header'], $table . ':' . $id, $page_propArr['header'], $newRow['pid']], $newRow['pid'], $NEW_id);
7197
                        }
7198
                        // Clear cache for relevant pages:
7199
                        $this->registerRecordIdForPageCacheClearing($table, $id);
7200
                    }
7201
                    return $id;
7202
                }
7203
                if ($this->enableLogging) {
7204
                    $this->log($table, $id, 1, 0, 2, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
7205
                }
7206
            }
7207
        }
7208
        return null;
7209
    }
7210
7211
    /**
7212
     * Checking stored record to see if the written values are properly updated.
7213
     *
7214
     * @param string $table Record table name
7215
     * @param int $id Record uid
7216
     * @param array $fieldArray Array of field=>value pairs to insert/update
7217
     * @param string $action Action, for logging only.
7218
     * @return array|null Selected row
7219
     * @see insertDB(), updateDB()
7220
     */
7221
    public function checkStoredRecord($table, $id, $fieldArray, $action)
7222
    {
7223
        $id = (int)$id;
7224
        if (is_array($GLOBALS['TCA'][$table]) && $id) {
7225
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7226
            $queryBuilder->getRestrictions()->removeAll();
7227
7228
            $row = $queryBuilder
7229
                ->select('*')
7230
                ->from($table)
7231
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7232
                ->execute()
7233
                ->fetch();
7234
7235
            if (!empty($row)) {
7236
                // Traverse array of values that was inserted into the database and compare with the actually stored value:
7237
                $errors = [];
7238
                foreach ($fieldArray as $key => $value) {
7239
                    if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
7240
                        if (is_float($row[$key])) {
7241
                            // if the database returns the value as double, compare it as double
7242
                            if ((double)$value !== (double)$row[$key]) {
7243
                                $errors[] = $key;
7244
                            }
7245
                        } else {
7246
                            $dbType = $GLOBALS['TCA'][$table]['columns'][$key]['config']['dbType'] ?? false;
7247
                            if ($dbType === 'datetime' || $dbType === 'time') {
7248
                                $row[$key] = $this->normalizeTimeFormat($table, $row[$key], $dbType);
7249
                            }
7250
                            if ((string)$value !== (string)$row[$key]) {
7251
                                // The is_numeric check catches cases where we want to store a float/double value
7252
                                // and database returns the field as a string with the least required amount of
7253
                                // significant digits, i.e. "0.00" being saved and "0" being read back.
7254
                                if (is_numeric($value) && is_numeric($row[$key])) {
7255
                                    if ((double)$value === (double)$row[$key]) {
7256
                                        continue;
7257
                                    }
7258
                                }
7259
                                $errors[] = $key;
7260
                            }
7261
                        }
7262
                    }
7263
                }
7264
                // Set log message if there were fields with unmatching values:
7265
                if (!empty($errors)) {
7266
                    $message = sprintf(
7267
                        '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.',
7268
                        $id,
7269
                        $table,
7270
                        implode(', ', $errors)
7271
                    );
7272
                    $this->log($table, $id, $action, 0, 1, $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

7272
                    $this->log($table, $id, /** @scrutinizer ignore-type */ $action, 0, 1, $message);
Loading history...
7273
                }
7274
                // Return selected rows:
7275
                return $row;
7276
            }
7277
        }
7278
        return null;
7279
    }
7280
7281
    /**
7282
     * Setting sys_history record, based on content previously set in $this->historyRecords[$table . ':' . $id] (by compareFieldArrayWithCurrentAndUnset())
7283
     *
7284
     * This functionality is now moved into the RecordHistoryStore and can be used instead.
7285
     *
7286
     * @param string $table Table name
7287
     * @param int $id Record ID
7288
     * @param int $logId Log entry ID, important for linking between log and history views
7289
     */
7290
    public function setHistory($table, $id, $logId)
7291
    {
7292
        if (isset($this->historyRecords[$table . ':' . $id])) {
7293
            $this->getRecordHistoryStore()->modifyRecord(
7294
                $table,
7295
                $id,
7296
                $this->historyRecords[$table . ':' . $id]
7297
            );
7298
        }
7299
    }
7300
7301
    /**
7302
     * @return RecordHistoryStore
7303
     */
7304
    protected function getRecordHistoryStore(): RecordHistoryStore
7305
    {
7306
        return GeneralUtility::makeInstance(
7307
            RecordHistoryStore::class,
7308
            RecordHistoryStore::USER_BACKEND,
0 ignored issues
show
Bug introduced by
TYPO3\CMS\Core\History\R...toryStore::USER_BACKEND of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

7308
            /** @scrutinizer ignore-type */ RecordHistoryStore::USER_BACKEND,
Loading history...
7309
            $this->BE_USER->user['uid'],
7310
            $this->BE_USER->user['ses_backuserid'] ?? null,
7311
            $this->BE_USER->workspace
0 ignored issues
show
Bug introduced by
$this->BE_USER->workspace of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

7311
            /** @scrutinizer ignore-type */ $this->BE_USER->workspace
Loading history...
7312
        );
7313
    }
7314
7315
    /**
7316
     * Update Reference Index (sys_refindex) for a record
7317
     * Should be called any almost any update to a record which could affect references inside the record.
7318
     *
7319
     * @param string $table Table name
7320
     * @param int $id Record UID
7321
     */
7322
    public function updateRefIndex($table, $id)
7323
    {
7324
        /** @var $refIndexObj ReferenceIndex */
7325
        $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
7326
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
7327
            $refIndexObj->setWorkspaceId($this->BE_USER->workspace);
7328
        }
7329
        $refIndexObj->enableRuntimeCache();
7330
        $refIndexObj->updateRefIndexTable($table, $id);
7331
    }
7332
7333
    /*********************************************
7334
     *
7335
     * Misc functions
7336
     *
7337
     ********************************************/
7338
    /**
7339
     * Returning sorting number for tables with a "sortby" column
7340
     * Using when new records are created and existing records are moved around.
7341
     *
7342
     * @param string $table Table name
7343
     * @param int $uid Uid of record to find sorting number for. May be zero in case of new.
7344
     * @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)
7345
     * @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.
7346
     */
7347
    public function getSortNumber($table, $uid, $pid)
7348
    {
7349
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
7350
            $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
7351
            $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
7352
            $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7353
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7354
7355
            $queryBuilder
7356
                ->select($sortRow, 'pid', 'uid')
7357
                ->from($table);
7358
7359
            // Sorting number is in the top
7360
            if ($pid >= 0) {
7361
                // Fetches the first record under this pid
7362
                $row = $queryBuilder
7363
                    ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7364
                    ->orderBy($sortRow, 'ASC')
7365
                    ->setMaxResults(1)
7366
                    ->execute()
7367
                    ->fetch();
7368
                // There was an element
7369
                if (!empty($row)) {
7370
                    // The top record was the record it self, so we return its current sortnumber
7371
                    if ($row['uid'] == $uid) {
7372
                        return $row[$sortRow];
7373
                    }
7374
                    // If the pages sortingnumber < 1 we must resort the records under this pid
7375
                    if ($row[$sortRow] < 1) {
7376
                        $this->resorting($table, $pid, $sortRow, 0);
7377
                        // First sorting number after resorting
7378
                        return $this->sortIntervals;
7379
                    }
7380
                    // Sorting number between current top element and zero
7381
                    return floor($row[$sortRow] / 2);
7382
                }
7383
                // No pages, so we choose the default value as sorting-number
7384
                // First sorting number if no elements.
7385
                return $this->sortIntervals;
7386
            }
7387
            // Sorting number is inside the list
7388
            // Fetches the record which is supposed to be the prev record
7389
            $row = $queryBuilder
7390
                    ->where($queryBuilder->expr()->eq(
7391
                        'uid',
7392
                        $queryBuilder->createNamedParameter(abs($pid), \PDO::PARAM_INT)
7393
                    ))
7394
                    ->execute()
7395
                    ->fetch();
7396
7397
            // There was a record
7398
            if (!empty($row)) {
7399
                // Look, if the record UID happens to be an offline record. If so, find its live version. Offline uids will be used when a page is versionized as "branch" so this is when we must correct - otherwise a pid of "-1" and a wrong sort-row number is returned which we don't want.
7400
                if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortRow . ',pid,uid')) {
7401
                    $row = $lookForLiveVersion;
7402
                }
7403
                // Fetch move placeholder, since it might point to a new page in the current workspace
7404
                if ($movePlaceholder = BackendUtility::getMovePlaceholder($table, $row['uid'], 'uid,pid,' . $sortRow)) {
7405
                    $row = $movePlaceholder;
7406
                }
7407
                // If the record should be inserted after itself, keep the current sorting information:
7408
                if ((int)$row['uid'] === (int)$uid) {
7409
                    $sortNumber = $row[$sortRow];
7410
                } else {
7411
                    $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7412
                    $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7413
7414
                    $subResults = $queryBuilder
7415
                            ->select($sortRow, 'pid', 'uid')
7416
                            ->from($table)
7417
                            ->where(
7418
                                $queryBuilder->expr()->eq(
7419
                                    'pid',
7420
                                    $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT)
7421
                                ),
7422
                                $queryBuilder->expr()->gte(
7423
                                    $sortRow,
7424
                                    $queryBuilder->createNamedParameter($row[$sortRow], \PDO::PARAM_INT)
7425
                                )
7426
                            )
7427
                            ->orderBy($sortRow, 'ASC')
7428
                            ->setMaxResults(2)
7429
                            ->execute()
7430
                            ->fetchAll();
7431
                    // Fetches the next record in order to calculate the in-between sortNumber
7432
                    // There was a record afterwards
7433
                    if (count($subResults) === 2) {
7434
                        // There was a record afterwards, fetch that
7435
                        $subrow = array_pop($subResults);
7436
                        // The sortNumber is found in between these values
7437
                        $sortNumber = $row[$sortRow] + floor(($subrow[$sortRow] - $row[$sortRow]) / 2);
7438
                        // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
7439
                        if ($sortNumber <= $row[$sortRow] || $sortNumber >= $subrow[$sortRow]) {
7440
                            // By this special param, resorting reserves and returns the sortnumber after the uid
7441
                            $sortNumber = $this->resorting($table, $row['pid'], $sortRow, $row['uid']);
7442
                        }
7443
                    } else {
7444
                        // If after the last record in the list, we just add the sortInterval to the last sortvalue
7445
                        $sortNumber = $row[$sortRow] + $this->sortIntervals;
7446
                    }
7447
                }
7448
                return ['pid' => $row['pid'], 'sortNumber' => $sortNumber];
7449
            }
7450
            if ($this->enableLogging) {
7451
                $propArr = $this->getRecordProperties($table, $uid);
7452
                // OK, don't insert $propArr['event_pid'] here...
7453
                $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) to after a non-existing record (uid=%s)', 1, [$propArr['header'], $table . ':' . $uid, abs($pid)], $propArr['pid']);
7454
            }
7455
            // There MUST be a page or else this cannot work
7456
            return false;
7457
        }
7458
        return null;
7459
    }
7460
7461
    /**
7462
     * Resorts a table.
7463
     * Used internally by getSortNumber()
7464
     *
7465
     * @param string $table Table name
7466
     * @param int $pid Pid in which to resort records.
7467
     * @param string $sortRow Sorting row
7468
     * @param int $return_SortNumber_After_This_Uid Uid of record from $table in this $pid and for which the return value will be set to a free sorting number after that record. This is used to return a sortingValue if the list is resorted because of inserting records inside the list and not in the top
7469
     * @return int|null If $return_SortNumber_After_This_Uid is set, will contain usable sorting number after that record if found (otherwise 0)
7470
     * @access private
7471
     * @see getSortNumber()
7472
     */
7473
    public function resorting($table, $pid, $sortRow, $return_SortNumber_After_This_Uid)
7474
    {
7475
        if ($GLOBALS['TCA'][$table] && $sortRow && $GLOBALS['TCA'][$table]['ctrl']['sortby'] == $sortRow) {
7476
            $returnVal = 0;
7477
            $intervals = $this->sortIntervals;
7478
            $i = $intervals * 2;
7479
            $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7480
            $queryBuilder = $connection->createQueryBuilder();
7481
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7482
7483
            $result = $queryBuilder
7484
                ->select('uid')
7485
                ->from($table)
7486
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7487
                ->orderBy($sortRow, 'ASC')
7488
                ->addOrderBy('uid', 'ASC')
7489
                ->execute();
7490
            while ($row = $result->fetch()) {
7491
                $uid = (int)$row['uid'];
7492
                if ($uid) {
7493
                    $connection->update($table, [$sortRow => $i], ['uid' => (int)$uid]);
7494
                    // This is used to return a sortingValue if the list is resorted because of inserting records inside the list and not in the top
7495
                    if ($uid == $return_SortNumber_After_This_Uid) {
7496
                        $i += $intervals;
7497
                        $returnVal = $i;
7498
                    }
7499
                } else {
7500
                    die('Fatal ERROR!! No Uid at resorting.');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
7501
                }
7502
                $i += $intervals;
7503
            }
7504
            return $returnVal;
7505
        }
7506
        return null;
7507
    }
7508
7509
    /**
7510
     * Returning uid of previous localized record, if any, for tables with a "sortby" column
7511
     * Used when new localized records are created so that localized records are sorted in the same order as the default language records
7512
     *
7513
     * @param string $table Table name
7514
     * @param int $uid Uid of default language record
7515
     * @param int $pid Pid of default language record
7516
     * @param int $language Language of localization
7517
     * @return int uid of record after which the localized record should be inserted
7518
     */
7519
    protected function getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
7520
    {
7521
        $previousLocalizedRecordUid = $uid;
7522
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
7523
            $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
7524
            $select = [$sortRow, 'pid', 'uid'];
7525
            // For content elements, we also need the colPos
7526
            if ($table === 'tt_content') {
7527
                $select[] = 'colPos';
7528
            }
7529
            // Get the sort value of the default language record
7530
            $row = BackendUtility::getRecord($table, $uid, implode(',', $select));
7531
            if (is_array($row)) {
7532
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7533
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7534
7535
                $queryBuilder
7536
                    ->select(...$select)
7537
                    ->from($table)
7538
                    ->where(
7539
                        $queryBuilder->expr()->eq(
7540
                            'pid',
7541
                            $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
7542
                        ),
7543
                        $queryBuilder->expr()->eq(
7544
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
7545
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
7546
                        ),
7547
                        $queryBuilder->expr()->lt(
7548
                            $sortRow,
7549
                            $queryBuilder->createNamedParameter($row[$sortRow], \PDO::PARAM_INT)
7550
                        )
7551
                    )
7552
                    ->orderBy($sortRow, 'DESC')
7553
                    ->setMaxResults(1);
7554
                if ($table === 'tt_content') {
7555
                    $queryBuilder
7556
                        ->andWhere(
7557
                            $queryBuilder->expr()->eq(
7558
                                'colPos',
7559
                                $queryBuilder->createNamedParameter($row['colPos'], \PDO::PARAM_INT)
7560
                            )
7561
                        );
7562
                }
7563
                // If there is an element, find its localized record in specified localization language
7564
                if ($previousRow = $queryBuilder->execute()->fetch()) {
7565
                    $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $language);
7566
                    if (is_array($previousLocalizedRecord[0])) {
7567
                        $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
7568
                    }
7569
                }
7570
            }
7571
        }
7572
        return $previousLocalizedRecordUid;
7573
    }
7574
7575
    /**
7576
     * Setting up perms_* fields in $fieldArray based on TSconfig input
7577
     * Used for new pages
7578
     *
7579
     * @param array $fieldArray Field Array, returned with modifications
7580
     * @param array $TSConfig_p TSconfig properties
7581
     * @return array Modified Field Array
7582
     */
7583
    public function setTSconfigPermissions($fieldArray, $TSConfig_p)
7584
    {
7585
        if ((string)$TSConfig_p['userid'] !== '') {
7586
            $fieldArray['perms_userid'] = (int)$TSConfig_p['userid'];
7587
        }
7588
        if ((string)$TSConfig_p['groupid'] !== '') {
7589
            $fieldArray['perms_groupid'] = (int)$TSConfig_p['groupid'];
7590
        }
7591
        if ((string)$TSConfig_p['user'] !== '') {
7592
            $fieldArray['perms_user'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['user']) ? $TSConfig_p['user'] : $this->assemblePermissions($TSConfig_p['user']);
7593
        }
7594
        if ((string)$TSConfig_p['group'] !== '') {
7595
            $fieldArray['perms_group'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['group']) ? $TSConfig_p['group'] : $this->assemblePermissions($TSConfig_p['group']);
7596
        }
7597
        if ((string)$TSConfig_p['everybody'] !== '') {
7598
            $fieldArray['perms_everybody'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['everybody']) ? $TSConfig_p['everybody'] : $this->assemblePermissions($TSConfig_p['everybody']);
7599
        }
7600
        return $fieldArray;
7601
    }
7602
7603
    /**
7604
     * 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.
7605
     * Used for new records and during copy operations for defaults
7606
     *
7607
     * @param string $table Table name for which to set default values.
7608
     * @return array Array with default values.
7609
     */
7610
    public function newFieldArray($table)
7611
    {
7612
        $fieldArray = [];
7613
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
7614
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $content) {
7615
                if (isset($this->defaultValues[$table][$field])) {
7616
                    $fieldArray[$field] = $this->defaultValues[$table][$field];
7617
                } elseif (isset($content['config']['default'])) {
7618
                    $fieldArray[$field] = $content['config']['default'];
7619
                }
7620
            }
7621
        }
7622
        // Set default permissions for a page.
7623
        if ($table === 'pages') {
7624
            $fieldArray['perms_userid'] = $this->userid;
7625
            $fieldArray['perms_groupid'] = (int)$this->BE_USER->firstMainGroup;
7626
            $fieldArray['perms_user'] = $this->assemblePermissions($this->defaultPermissions['user']);
7627
            $fieldArray['perms_group'] = $this->assemblePermissions($this->defaultPermissions['group']);
7628
            $fieldArray['perms_everybody'] = $this->assemblePermissions($this->defaultPermissions['everybody']);
7629
        }
7630
        return $fieldArray;
7631
    }
7632
7633
    /**
7634
     * 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.
7635
     *
7636
     * @param string $table Table name
7637
     * @param array $incomingFieldArray Incoming array (passed by reference)
7638
     */
7639
    public function addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
7640
    {
7641
        // Checking languages:
7642
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
7643
            if (!isset($incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
7644
                // Language field must be found in input row - otherwise it does not make sense.
7645
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
7646
                    ->getQueryBuilderForTable('sys_language');
7647
                $queryBuilder->getRestrictions()
7648
                    ->removeAll()
7649
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7650
                $queryBuilder
7651
                    ->select('uid')
7652
                    ->from('sys_language')
7653
                    ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
7654
                $rows = array_merge([['uid' => 0]], $queryBuilder->execute()->fetchAll(), [['uid' => -1]]);
7655
                foreach ($rows as $r) {
7656
                    if ($this->BE_USER->checkLanguageAccess($r['uid'])) {
7657
                        $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $r['uid'];
7658
                        break;
7659
                    }
7660
                }
7661
            }
7662
        }
7663
    }
7664
7665
    /**
7666
     * Returns the $data array from $table overridden in the fields defined in ->overrideValues.
7667
     *
7668
     * @param string $table Table name
7669
     * @param array $data Data array with fields from table. These will be overlaid with values in $this->overrideValues[$table]
7670
     * @return array Data array, processed.
7671
     */
7672
    public function overrideFieldArray($table, $data)
7673
    {
7674
        if (is_array($this->overrideValues[$table])) {
7675
            $data = array_merge($data, $this->overrideValues[$table]);
7676
        }
7677
        return $data;
7678
    }
7679
7680
    /**
7681
     * Compares the incoming field array with the current record and unsets all fields which are the same.
7682
     * Used for existing records being updated
7683
     *
7684
     * @param string $table Record table name
7685
     * @param int $id Record uid
7686
     * @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!
7687
     * @return array Returns $fieldArray. If the returned array is empty, then the record should not be updated!
7688
     */
7689
    public function compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
7690
    {
7691
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7692
        $queryBuilder = $connection->createQueryBuilder();
7693
        $queryBuilder->getRestrictions()->removeAll();
7694
        $currentRecord = $queryBuilder->select('*')
7695
            ->from($table)
7696
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7697
            ->execute()
7698
            ->fetch();
7699
        // If the current record exists (which it should...), begin comparison:
7700
        if (is_array($currentRecord)) {
7701
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
7702
            $columnRecordTypes = [];
7703
            foreach ($currentRecord as $columnName => $_) {
7704
                $columnRecordTypes[$columnName] = '';
7705
                $type = $tableDetails->getColumn($columnName)->getType();
7706
                if ($type instanceof IntegerType) {
7707
                    $columnRecordTypes[$columnName] = 'int';
7708
                }
7709
            }
7710
            // Unset the fields which are similar:
7711
            foreach ($fieldArray as $col => $val) {
7712
                $fieldConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
7713
                $isNullField = (!empty($fieldConfiguration['eval']) && GeneralUtility::inList($fieldConfiguration['eval'], 'null'));
7714
7715
                // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
7716
                // In general this avoids to store superfluous data which also will be visualized in the editing history.
7717
                if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $columnRecordTypes[$col], $isNullField)) {
7718
                    unset($fieldArray[$col]);
7719
                } else {
7720
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
7721
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
7722
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
7723
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
7724
                    }
7725
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
7726
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
7727
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
7728
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
7729
                    }
7730
                }
7731
            }
7732
        } else {
7733
            // If the current record does not exist this is an error anyways and we just return an empty array here.
7734
            $fieldArray = [];
7735
        }
7736
        return $fieldArray;
7737
    }
7738
7739
    /**
7740
     * Determines whether submitted values and stored values are equal.
7741
     * This prevents from adding superfluous field changes which would be shown in the record history as well.
7742
     * For NULL fields (see accordant TCA definition 'eval' = 'null'), a special handling is required since
7743
     * (!strcmp(NULL, '')) would be a false-positive.
7744
     *
7745
     * @param mixed $submittedValue Value that has submitted (e.g. from a backend form)
7746
     * @param mixed $storedValue Value that is currently stored in the database
7747
     * @param string $storedType SQL type of the stored value column (see mysql_field_type(), e.g 'int', 'string',  ...)
7748
     * @param bool $allowNull Whether NULL values are allowed by accordant TCA definition ('eval' = 'null')
7749
     * @return bool Whether both values are considered to be equal
7750
     */
7751
    protected function isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
7752
    {
7753
        // No NULL values are allowed, this is the regular behaviour.
7754
        // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
7755
        if (!$allowNull) {
7756
            $result = (string)$submittedValue === (string)$storedValue || $storedType === 'int' && (int)$storedValue === (int)$submittedValue;
7757
            // Null values are allowed, but currently there's a real (not NULL) value.
7758
        // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
7759
        } elseif ($storedValue !== null) {
7760
            $result = (
7761
                $submittedValue !== null
7762
                && $this->isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
7763
            );
7764
            // Null values are allowed, and currently there's a NULL value.
7765
        // Thus, check whether a NULL value was submitted.
7766
        } else {
7767
            $result = ($submittedValue === null);
7768
        }
7769
7770
        return $result;
7771
    }
7772
7773
    /**
7774
     * Calculates the bitvalue of the permissions given in a string, comma-separated
7775
     *
7776
     * @param string $string List of pMap strings
7777
     * @return int Integer mask
7778
     * @see setTSconfigPermissions(), newFieldArray()
7779
     */
7780
    public function assemblePermissions($string)
7781
    {
7782
        $keyArr = GeneralUtility::trimExplode(',', $string, true);
7783
        $value = 0;
7784
        foreach ($keyArr as $key) {
7785
            if ($key && isset($this->pMap[$key])) {
7786
                $value |= $this->pMap[$key];
7787
            }
7788
        }
7789
        return $value;
7790
    }
7791
7792
    /**
7793
     * Converts a HTML entity (like &#123;) to the character '123'
7794
     *
7795
     * @param string $input Input string
7796
     * @return string Output string
7797
     */
7798
    public function convNumEntityToByteValue($input)
7799
    {
7800
        $token = md5(microtime());
7801
        $parts = explode($token, preg_replace('/(&#([0-9]+);)/', $token . '\\2' . $token, $input));
7802
        foreach ($parts as $k => $v) {
7803
            if ($k % 2) {
7804
                $v = (int)$v;
7805
                // Just to make sure that control bytes are not converted.
7806
                if ($v > 32) {
7807
                    $parts[$k] = chr($v);
7808
                }
7809
            }
7810
        }
7811
        return implode('', $parts);
7812
    }
7813
7814
    /**
7815
     * Disables the delete clause for fetching records.
7816
     * In general only undeleted records will be used. If the delete
7817
     * clause is disabled, also deleted records are taken into account.
7818
     */
7819
    public function disableDeleteClause()
7820
    {
7821
        $this->disableDeleteClause = true;
7822
    }
7823
7824
    /**
7825
     * Returns delete-clause for the $table
7826
     *
7827
     * @param string $table Table name
7828
     * @return string Delete clause
7829
     */
7830
    public function deleteClause($table)
7831
    {
7832
        // Returns the proper delete-clause if any for a table from TCA
7833
        if (!$this->disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
7834
            return ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
7835
        }
7836
        return '';
7837
    }
7838
7839
    /**
7840
     * Add delete restriction if not disabled
7841
     *
7842
     * @param QueryRestrictionContainerInterface $restrictions
7843
     */
7844
    protected function addDeleteRestriction(QueryRestrictionContainerInterface $restrictions)
7845
    {
7846
        if (!$this->disableDeleteClause) {
7847
            $restrictions->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7848
        }
7849
    }
7850
7851
    /**
7852
     * Gets UID of parent record. If record is deleted it will be looked up in
7853
     * an array built before the record was deleted
7854
     *
7855
     * @param string $table Table where record lives/lived
7856
     * @param int $uid Record UID
7857
     * @return int[] Parent UIDs
7858
     */
7859
    protected function getOriginalParentOfRecord($table, $uid)
7860
    {
7861
        if (isset(self::$recordPidsForDeletedRecords[$table][$uid])) {
7862
            return self::$recordPidsForDeletedRecords[$table][$uid];
7863
        }
7864
        list($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

7864
        list($parentUid) = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
7865
        return [$parentUid];
7866
    }
7867
7868
    /**
7869
     * Return TSconfig for a page id
7870
     *
7871
     * @param int $tscPID Page id (PID) from which to get configuration.
7872
     * @return array TSconfig array, if any
7873
     */
7874
    public function getTCEMAIN_TSconfig($tscPID)
7875
    {
7876
        if (!isset($this->cachedTSconfig[$tscPID])) {
7877
            $this->cachedTSconfig[$tscPID] = $this->BE_USER->getTSConfig('TCEMAIN', BackendUtility::getPagesTSconfig($tscPID));
7878
        }
7879
        return $this->cachedTSconfig[$tscPID]['properties'];
7880
    }
7881
7882
    /**
7883
     * Extract entries from TSconfig for a specific table. This will merge specific and default configuration together.
7884
     *
7885
     * @param string $table Table name
7886
     * @param array $TSconfig TSconfig for page
7887
     * @return array TSconfig merged
7888
     * @see getTCEMAIN_TSconfig()
7889
     */
7890
    public function getTableEntries($table, $TSconfig)
7891
    {
7892
        $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : [];
7893
        $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : [];
7894
        ArrayUtility::mergeRecursiveWithOverrule($dA, $tA);
7895
        return $dA;
7896
    }
7897
7898
    /**
7899
     * Returns the pid of a record from $table with $uid
7900
     *
7901
     * @param string $table Table name
7902
     * @param int $uid Record uid
7903
     * @return int|false PID value (unless the record did not exist in which case FALSE is returned)
7904
     */
7905
    public function getPID($table, $uid)
7906
    {
7907
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7908
        $queryBuilder->getRestrictions()
7909
            ->removeAll();
7910
        $queryBuilder->select('pid')
7911
            ->from($table)
7912
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
7913
        if ($row = $queryBuilder->execute()->fetch()) {
7914
            return $row['pid'];
7915
        }
7916
        return false;
7917
    }
7918
7919
    /**
7920
     * Executing dbAnalysisStore
7921
     * This will save MM relations for new records but is executed after records are created because we need to know the ID of them
7922
     */
7923
    public function dbAnalysisStoreExec()
7924
    {
7925
        foreach ($this->dbAnalysisStore as $action) {
7926
            $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
7927
            if ($id) {
7928
                $action[0]->writeMM($action[1], $id, $action[3]);
7929
            }
7930
        }
7931
    }
7932
7933
    /**
7934
     * Removing files registered for removal before exit
7935
     */
7936
    public function removeRegisteredFiles()
7937
    {
7938
        foreach ($this->removeFilesStore as $file) {
7939
            if (@is_file($file)) {
7940
                $file = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
7941
                $file->delete();
7942
            }
7943
        }
7944
    }
7945
7946
    /**
7947
     * Returns array, $CPtable, of pages under the $pid going down to $counter levels.
7948
     * Selecting ONLY pages which the user has read-access to!
7949
     *
7950
     * @param array $CPtable Accumulation of page uid=>pid pairs in branch of $pid
7951
     * @param int $pid Page ID for which to find subpages
7952
     * @param int $counter Number of levels to go down.
7953
     * @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!
7954
     * @return array Return array.
7955
     */
7956
    public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
7957
    {
7958
        if ($counter) {
7959
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
7960
            $restrictions = $queryBuilder->getRestrictions()->removeAll();
7961
            $this->addDeleteRestriction($restrictions);
7962
            $queryBuilder
7963
                ->select('uid')
7964
                ->from('pages')
7965
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7966
                ->orderBy('sorting', 'DESC');
7967
            if (!$this->admin) {
7968
                $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($this->pMap['show']));
7969
            }
7970
            if ((int)$this->BE_USER->workspace === 0) {
7971
                $queryBuilder->andWhere(
7972
                    $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
7973
                );
7974
            } else {
7975
                $queryBuilder->andWhere($queryBuilder->expr()->in(
7976
                    't3ver_wsid',
7977
                    $queryBuilder->createNamedParameter([0, $this->BE_USER->workspace], Connection::PARAM_INT_ARRAY)
7978
                ));
7979
            }
7980
            $result = $queryBuilder->execute();
7981
7982
            $pages = [];
7983
            while ($row = $result->fetch()) {
7984
                $pages[$row['uid']] = $row;
7985
            }
7986
7987
            // Resolve placeholders of workspace versions
7988
            if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
7989
                $pages = array_reverse(
7990
                    $this->resolveVersionedRecords(
7991
                        'pages',
7992
                        'uid',
7993
                        'sorting',
7994
                        array_keys($pages)
7995
                    ),
7996
                    true
7997
                );
7998
            }
7999
8000
            foreach ($pages as $page) {
8001
                if ($page['uid'] != $rootID) {
8002
                    $CPtable[$page['uid']] = $pid;
8003
                    // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
8004
                    if ($counter - 1) {
8005
                        $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
8006
                    }
8007
                }
8008
            }
8009
        }
8010
        return $CPtable;
8011
    }
8012
8013
    /**
8014
     * List of all tables (those administrators has access to = array_keys of $GLOBALS['TCA'])
8015
     *
8016
     * @return array Array of all TCA table names
8017
     */
8018
    public function compileAdminTables()
8019
    {
8020
        return array_keys($GLOBALS['TCA']);
8021
    }
8022
8023
    /**
8024
     * Checks if any uniqueInPid eval input fields are in the record and if so, they are re-written to be correct.
8025
     *
8026
     * @param string $table Table name
8027
     * @param int $uid Record UID
8028
     */
8029
    public function fixUniqueInPid($table, $uid)
8030
    {
8031
        if (empty($GLOBALS['TCA'][$table])) {
8032
            return;
8033
        }
8034
8035
        $curData = $this->recordInfo($table, $uid, '*');
8036
        $newData = [];
8037
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
8038
            if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
8039
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
8040
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
8041
                    $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
8042
                    if ((string)$newV !== (string)$curData[$field]) {
8043
                        $newData[$field] = $newV;
8044
                    }
8045
                }
8046
            }
8047
        }
8048
        // IF there are changed fields, then update the database
8049
        if (!empty($newData)) {
8050
            $this->updateDB($table, $uid, $newData);
8051
        }
8052
    }
8053
8054
    /**
8055
     * When er record is copied you can specify fields from the previous record which should be copied into the new one
8056
     * 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)
8057
     *
8058
     * @param string $table Table name
8059
     * @param int $uid Record UID
8060
     * @param int $prevUid UID of previous record
8061
     * @param bool $update If set, updates the record
8062
     * @param array $newData Input array. If fields are already specified AND $update is not set, values are not set in output array.
8063
     * @return array Output array (For when the copying operation needs to get the information instead of updating the info)
8064
     */
8065
    public function fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData = [])
8066
    {
8067
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
8068
            $prevData = $this->recordInfo($table, $prevUid, '*');
8069
            $theFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
8070
            foreach ($theFields as $field) {
8071
                if ($GLOBALS['TCA'][$table]['columns'][$field] && ($update || !isset($newData[$field]))) {
8072
                    $newData[$field] = $prevData[$field];
8073
                }
8074
            }
8075
            if ($update && !empty($newData)) {
8076
                $this->updateDB($table, $uid, $newData);
8077
            }
8078
        }
8079
        return $newData;
8080
    }
8081
8082
    /**
8083
     * Returns all fieldnames from a table which are a list of files
8084
     *
8085
     * @param string $table Table name
8086
     * @return array Array of fieldnames that are either "group" or "file" types.
8087
     */
8088
    public function extFileFields($table)
8089
    {
8090
        $listArr = [];
8091
        if (isset($GLOBALS['TCA'][$table]['columns'])) {
8092
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) {
8093
                if ($configArr['config']['type'] === 'group' && ($configArr['config']['internal_type'] === 'file' || $configArr['config']['internal_type'] === 'file_reference')) {
8094
                    $listArr[] = $field;
8095
                }
8096
            }
8097
        }
8098
        return $listArr;
8099
    }
8100
8101
    /**
8102
     * Casts a reference value. In case MM relations or foreign_field
8103
     * references are used. All other configurations, as well as
8104
     * foreign_table(!) could be stored as comma-separated-values
8105
     * as well. Since the system is not able to determine the default
8106
     * value automatically then, the TCA default value is used if
8107
     * it has been defined.
8108
     *
8109
     * @param int|string $value The value to be casted (e.g. '', '0', '1,2,3')
8110
     * @param array $configuration The TCA configuration of the accordant field
8111
     * @return int|string
8112
     */
8113
    protected function castReferenceValue($value, array $configuration)
8114
    {
8115
        if ((string)$value !== '') {
8116
            return $value;
8117
        }
8118
8119
        if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
8120
            return 0;
8121
        }
8122
8123
        if (array_key_exists('default', $configuration)) {
8124
            return $configuration['default'];
8125
        }
8126
8127
        return $value;
8128
    }
8129
8130
    /**
8131
     * Returns TRUE if the TCA/columns field type is a DB reference field
8132
     *
8133
     * @param array $conf Config array for TCA/columns field
8134
     * @return bool TRUE if DB reference field (group/db or select with foreign-table)
8135
     */
8136
    public function isReferenceField($conf)
8137
    {
8138
        return $conf['type'] === 'group' && $conf['internal_type'] === 'db' || $conf['type'] === 'select' && $conf['foreign_table'];
8139
    }
8140
8141
    /**
8142
     * Returns the subtype as a string of an inline field.
8143
     * If it's not an inline field at all, it returns FALSE.
8144
     *
8145
     * @param array $conf Config array for TCA/columns field
8146
     * @return string|bool string Inline subtype (field|mm|list), boolean: FALSE
8147
     */
8148
    public function getInlineFieldType($conf)
8149
    {
8150
        if ($conf['type'] !== 'inline' || !$conf['foreign_table']) {
8151
            return false;
8152
        }
8153
        if ($conf['foreign_field']) {
8154
            // The reference to the parent is stored in a pointer field in the child record
8155
            return 'field';
8156
        }
8157
        if ($conf['MM']) {
8158
            // Regular MM intermediate table is used to store data
8159
            return 'mm';
8160
        }
8161
        // An item list (separated by comma) is stored (like select type is doing)
8162
        return 'list';
8163
    }
8164
8165
    /**
8166
     * Get modified header for a copied record
8167
     *
8168
     * @param string $table Table name
8169
     * @param int $pid PID value in which other records to test might be
8170
     * @param string $field Field name to get header value for.
8171
     * @param string $value Current field value
8172
     * @param int $count Counter (number of recursions)
8173
     * @param string $prevTitle Previous title we checked for (in previous recursion)
8174
     * @return string The field value, possibly appended with a "copy label
8175
     */
8176
    public function getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
8177
    {
8178
        // Set title value to check for:
8179
        $checkTitle = $value;
8180
        if ($count > 0) {
8181
            $checkTitle = $value . rtrim(' ' . sprintf($this->prependLabel($table), $count));
8182
        }
8183
        // Do check:
8184
        if ($prevTitle != $checkTitle || $count < 100) {
8185
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8186
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
8187
            $rowCount = $queryBuilder
8188
                ->count('uid')
8189
                ->from($table)
8190
                ->where(
8191
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
8192
                    $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($checkTitle, \PDO::PARAM_STR))
8193
                )
8194
                ->execute()
8195
                ->fetchColumn(0);
8196
            if ($rowCount) {
8197
                return $this->getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
8198
            }
8199
        }
8200
        // Default is to just return the current input title if no other was returned before:
8201
        return $checkTitle;
8202
    }
8203
8204
    /**
8205
     * Return "copy" label for a table. Although the name is "prepend" it actually APPENDs the label (after ...)
8206
     *
8207
     * @param string $table Table name
8208
     * @return string Label to append, containing "%s" for the number
8209
     * @see getCopyHeader()
8210
     */
8211
    public function prependLabel($table)
8212
    {
8213
        return $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
8214
    }
8215
8216
    /**
8217
     * Get the final pid based on $table and $pid ($destPid type... pos/neg)
8218
     *
8219
     * @param string $table Table name
8220
     * @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!
8221
     * @return int
8222
     */
8223
    public function resolvePid($table, $pid)
8224
    {
8225
        $pid = (int)$pid;
8226
        if ($pid < 0) {
8227
            $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8228
            $query->getRestrictions()
8229
                ->removeAll();
8230
            $row = $query
8231
                ->select('pid')
8232
                ->from($table)
8233
                ->where($query->expr()->eq('uid', $query->createNamedParameter(abs($pid), \PDO::PARAM_INT)))
8234
                ->execute()
8235
                ->fetch();
8236
            // Look, if the record UID happens to be an offline record. If so, find its live version.
8237
            // Offline uids will be used when a page is versionized as "branch" so this is when we
8238
            // must correct - otherwise a pid of "-1" and a wrong sort-row number
8239
            // is returned which we don't want.
8240
            if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, abs($pid), 'pid')) {
0 ignored issues
show
Bug introduced by
It seems like abs($pid) can also be of type double; however, parameter $uid of TYPO3\CMS\Backend\Utilit...etLiveVersionOfRecord() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

8240
            if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, /** @scrutinizer ignore-type */ abs($pid), 'pid')) {
Loading history...
8241
                $row = $lookForLiveVersion;
8242
            }
8243
            $pid = (int)$row['pid'];
8244
        }
8245
        return $pid;
8246
    }
8247
8248
    /**
8249
     * Removes the prependAtCopy prefix on values
8250
     *
8251
     * @param string $table Table name
8252
     * @param string $value The value to fix
8253
     * @return string Clean name
8254
     */
8255
    public function clearPrefixFromValue($table, $value)
8256
    {
8257
        $regex = '/' . sprintf(quotemeta($this->prependLabel($table)), '[0-9]*') . '$/';
8258
        return @preg_replace($regex, '', $value);
8259
    }
8260
8261
    /**
8262
     * File functions on external file references. eg. deleting files when deleting record
8263
     *
8264
     * @param string $table Table name
8265
     * @param string $field Field name
8266
     * @param string $filelist List of files to work on from field
8267
     */
8268
    public function extFileFunctions($table, $field, $filelist)
8269
    {
8270
        $uploadFolder = $GLOBALS['TCA'][$table]['columns'][$field]['config']['uploadfolder'];
8271
        if ($uploadFolder && trim($filelist) && $GLOBALS['TCA'][$table]['columns'][$field]['config']['internal_type'] === 'file') {
8272
            $uploadPath = PATH_site . $uploadFolder;
8273
            $fileArray = GeneralUtility::trimExplode(',', $filelist, true);
8274
            foreach ($fileArray as $theFile) {
8275
                $theFileFullPath = $uploadPath . '/' . $theFile;
8276
                if (@is_file($theFileFullPath)) {
8277
                    $this->getResourceFactory()->retrieveFileOrFolderObject($theFileFullPath)->delete();
8278
                } else {
8279
                    $this->log($table, 0, 3, 0, 100, 'Delete: Referenced file that was supposed to be deleted together with it\'s record didn\'t exist');
8280
                }
8281
            }
8282
        }
8283
    }
8284
8285
    /**
8286
     * Check if there are records from tables on the pages to be deleted which the current user is not allowed to
8287
     *
8288
     * @param int[] $pageIds IDs of pages which should be checked
8289
     * @return bool Return TRUE, if permission granted
8290
     * @see canDeletePage()
8291
     */
8292
    protected function checkForRecordsFromDisallowedTables(array $pageIds)
8293
    {
8294
        if ($this->admin) {
8295
            return true;
8296
        }
8297
8298
        if (!empty($pageIds)) {
8299
            $tableNames = $this->compileAdminTables();
8300
            foreach ($tableNames as $table) {
8301
                $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8302
                $query->getRestrictions()
8303
                    ->removeAll()
8304
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8305
                $count = $query->count('uid')
8306
                    ->from($table)
8307
                    ->where($query->expr()->in(
8308
                        'pid',
8309
                        $query->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
8310
                    ))
8311
                    ->execute()
8312
                    ->fetchColumn(0);
8313
                if ($count && ($this->tableReadOnly($table) || !$this->checkModifyAccessList($table))) {
8314
                    return false;
8315
                }
8316
            }
8317
        }
8318
        return true;
8319
    }
8320
8321
    /**
8322
     * Determine if a record was copied or if a record is the result of a copy action.
8323
     *
8324
     * @param string $table The tablename of the record
8325
     * @param int $uid The uid of the record
8326
     * @return bool Returns TRUE if the record is copied or is the result of a copy action
8327
     */
8328
    public function isRecordCopied($table, $uid)
8329
    {
8330
        // If the record was copied:
8331
        if (isset($this->copyMappingArray[$table][$uid])) {
8332
            return true;
8333
        }
8334
        if (isset($this->copyMappingArray[$table]) && in_array($uid, array_values($this->copyMappingArray[$table]))) {
8335
            return true;
8336
        }
8337
        return false;
8338
    }
8339
8340
    /******************************
8341
     *
8342
     * Clearing cache
8343
     *
8344
     ******************************/
8345
8346
    /**
8347
     * Clearing the cache based on a page being updated
8348
     * If the $table is 'pages' then cache is cleared for all pages on the same level (and subsequent?)
8349
     * Else just clear the cache for the parent page of the record.
8350
     *
8351
     * @param string $table Table name of record that was just updated.
8352
     * @param int $uid UID of updated / inserted record
8353
     * @param int $pid REAL PID of page of a deleted/moved record to get TSconfig in ClearCache.
8354
     * @internal This method is not meant to be called directly but only from the core itself or from hooks
8355
     */
8356
    public function registerRecordIdForPageCacheClearing($table, $uid, $pid = null)
8357
    {
8358
        if (!is_array(static::$recordsToClearCacheFor[$table])) {
8359
            static::$recordsToClearCacheFor[$table] = [];
8360
        }
8361
        static::$recordsToClearCacheFor[$table][] = (int)$uid;
8362
        if ($pid !== null) {
8363
            if (!is_array(static::$recordPidsForDeletedRecords[$table])) {
8364
                static::$recordPidsForDeletedRecords[$table] = [];
8365
            }
8366
            static::$recordPidsForDeletedRecords[$table][$uid][] = (int)$pid;
8367
        }
8368
    }
8369
8370
    /**
8371
     * Do the actual clear cache
8372
     */
8373
    protected function processClearCacheQueue()
8374
    {
8375
        $tagsToClear = [];
8376
        $clearCacheCommands = [];
8377
8378
        foreach (static::$recordsToClearCacheFor as $table => $uids) {
8379
            foreach (array_unique($uids) as $uid) {
8380
                if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
8381
                    return;
8382
                }
8383
                // For move commands we may get more then 1 parent.
8384
                $pageUids = $this->getOriginalParentOfRecord($table, $uid);
8385
                foreach ($pageUids as $originalParent) {
8386
                    list($tagsToClearFromPrepare, $clearCacheCommandsFromPrepare)
8387
                        = $this->prepareCacheFlush($table, $uid, $originalParent);
8388
                    $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
8389
                    $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
8390
                }
8391
            }
8392
        }
8393
8394
        /** @var CacheManager $cacheManager */
8395
        $cacheManager = $this->getCacheManager();
8396
        $cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear));
8397
8398
        // Filter duplicate cache commands from cacheQueue
8399
        $clearCacheCommands = array_unique($clearCacheCommands);
8400
        // Execute collected clear cache commands from page TSConfig
8401
        foreach ($clearCacheCommands as $command) {
8402
            $this->clear_cacheCmd($command);
8403
        }
8404
8405
        // Reset the cache clearing array
8406
        static::$recordsToClearCacheFor = [];
8407
8408
        // Reset the original pid array
8409
        static::$recordPidsForDeletedRecords = [];
8410
    }
8411
8412
    /**
8413
     * Prepare the cache clearing
8414
     *
8415
     * @param string $table Table name of record that needs to be cleared
8416
     * @param int $uid UID of record for which the cache needs to be cleared
8417
     * @param int $pid Original pid of the page of the record which the cache needs to be cleared
8418
     * @return array Array with tagsToClear and clearCacheCommands
8419
     * @internal This function is internal only it may be changed/removed also in minor version numbers.
8420
     */
8421
    protected function prepareCacheFlush($table, $uid, $pid)
8422
    {
8423
        $tagsToClear = [];
8424
        $clearCacheCommands = [];
8425
        $pageUid = 0;
8426
        // Get Page TSconfig relevant:
8427
        $TSConfig = $this->getTCEMAIN_TSconfig($pid);
8428
        if (empty($TSConfig['clearCache_disable'])) {
8429
            // If table is "pages":
8430
            $pageIdsThatNeedCacheFlush = [];
8431
            if ($table === 'pages') {
8432
                // Find out if the record is a get the original page
8433
                $pageUid = $this->getDefaultLanguagePageId($uid);
8434
8435
                // Builds list of pages on the SAME level as this page (siblings)
8436
                $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
8437
                $queryBuilder = $connectionPool->getQueryBuilderForTable('pages');
8438
                $queryBuilder->getRestrictions()
8439
                    ->removeAll()
8440
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8441
                $siblings = $queryBuilder
8442
                    ->select('A.pid AS pid', 'B.uid AS uid')
8443
                    ->from('pages', 'A')
8444
                    ->from('pages', 'B')
8445
                    ->where(
8446
                        $queryBuilder->expr()->eq('A.uid', $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)),
8447
                        $queryBuilder->expr()->eq('B.pid', $queryBuilder->quoteIdentifier('A.pid')),
8448
                        $queryBuilder->expr()->gte('A.pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8449
                    )
8450
                    ->execute();
8451
8452
                $pid_tmp = 0;
8453
                while ($row_tmp = $siblings->fetch()) {
8454
                    $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
8455
                    $pid_tmp = (int)$row_tmp['pid'];
8456
                    // Add children as well:
8457
                    if ($TSConfig['clearCache_pageSiblingChildren']) {
8458
                        $siblingChildrenQuery = $connectionPool->getQueryBuilderForTable('pages');
8459
                        $siblingChildrenQuery->getRestrictions()
8460
                            ->removeAll()
8461
                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8462
                        $siblingChildren = $siblingChildrenQuery
8463
                            ->select('uid')
8464
                            ->from('pages')
8465
                            ->where($siblingChildrenQuery->expr()->eq(
8466
                                'pid',
8467
                                $siblingChildrenQuery->createNamedParameter($row_tmp['uid'], \PDO::PARAM_INT)
8468
                            ))
8469
                            ->execute();
8470
                        while ($row_tmp2 = $siblingChildren->fetch()) {
8471
                            $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
8472
                        }
8473
                    }
8474
                }
8475
                // Finally, add the parent page as well:
8476
                if ($pid_tmp > 0) {
8477
                    $pageIdsThatNeedCacheFlush[] = $pid_tmp;
8478
                }
8479
                // Add grand-parent as well:
8480
                if ($TSConfig['clearCache_pageGrandParent']) {
8481
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8482
                    $parentQuery->getRestrictions()
8483
                        ->removeAll()
8484
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8485
                    $row_tmp = $parentQuery
8486
                        ->select('pid')
8487
                        ->from('pages')
8488
                        ->where($parentQuery->expr()->eq(
8489
                            'uid',
8490
                            $parentQuery->createNamedParameter($pid_tmp, \PDO::PARAM_INT)
8491
                        ))
8492
                        ->execute()
8493
                        ->fetch();
8494
                    if (!empty($row_tmp)) {
8495
                        $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
8496
                    }
8497
                }
8498
            } else {
8499
                // For other tables than "pages", delete cache for the records "parent page".
8500
                $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
8501
            }
8502
            // Call pre-processing function for clearing of cache for page ids:
8503
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8504
                $_params = ['pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()'];
8505
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8506
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8507
            }
8508
            // Delete cache for selected pages:
8509
            foreach ($pageIdsThatNeedCacheFlush as $pageId) {
8510
                // Workspaces always use "-1" as the page id which do not
8511
                // point to real pages and caches at all. Flushing caches for
8512
                // those records does not make sense and decreases performance
8513
                if ($pageId >= 0) {
8514
                    $tagsToClear['pageId_' . $pageId] = true;
8515
                }
8516
            }
8517
            // Queue delete cache for current table and record
8518
            $tagsToClear[$table] = true;
8519
            $tagsToClear[$table . '_' . $uid] = true;
8520
        }
8521
        // Clear cache for pages entered in TSconfig:
8522
        if (!empty($TSConfig['clearCacheCmd'])) {
8523
            $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
8524
            $clearCacheCommands = array_unique($commands);
8525
        }
8526
        // Call post processing function for clear-cache:
8527
        $_params = ['table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig];
8528
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8529
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8530
        }
8531
        return [
8532
            $tagsToClear,
8533
            $clearCacheCommands
8534
        ];
8535
    }
8536
8537
    /**
8538
     * Clears the cache based on the command $cacheCmd.
8539
     *
8540
     * $cacheCmd='pages'
8541
     * Clears cache for all pages and page-based caches inside the cache manager.
8542
     * Requires admin-flag to be set for BE_USER.
8543
     *
8544
     * $cacheCmd='all'
8545
     * Clears all cache_tables. This is necessary if templates are updated.
8546
     * Requires admin-flag to be set for BE_USER.
8547
     *
8548
     * The following cache_* are intentionally not cleared by 'all'
8549
     *
8550
     * - cache_imagesizes:	Clearing this table would cause a lot of unneeded
8551
     * Imagemagick calls because the size informations have
8552
     * to be fetched again after clearing.
8553
     * - all caches inside the cache manager that are inside the group "system"
8554
     * - they are only needed to build up the core system and templates,
8555
     *   use "temp_cached" or "system" to do that
8556
     *
8557
     * $cacheCmd=[integer]
8558
     * Clears cache for the page pointed to by $cacheCmd (an integer).
8559
     *
8560
     * $cacheCmd='cacheTag:[string]'
8561
     * Flush page and pagesection cache by given tag
8562
     *
8563
     * $cacheCmd='cacheId:[string]'
8564
     * Removes cache identifier from page and page section cache
8565
     *
8566
     * Can call a list of post processing functions as defined in
8567
     * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc']
8568
     * (numeric array with values being the function references, called by
8569
     * GeneralUtility::callUserFunction()).
8570
     *
8571
     *
8572
     * @param string $cacheCmd The cache command, see above description
8573
     */
8574
    public function clear_cacheCmd($cacheCmd)
8575
    {
8576
        if (is_object($this->BE_USER)) {
8577
            $this->BE_USER->writelog(3, 1, 0, 0, 'User %s has cleared the cache (cacheCmd=%s)', [$this->BE_USER->user['username'], $cacheCmd]);
8578
        }
8579
        switch (strtolower($cacheCmd)) {
8580
            case 'pages':
8581
                if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.pages')) {
8582
                    $this->getCacheManager()->flushCachesInGroup('pages');
8583
                }
8584
                break;
8585
            case 'all':
8586
                // allow to clear all caches if the TS config option is enabled or the option is not explicitly
8587
                // disabled for admins (which could clear all caches by default). The latter option is useful
8588
                // for big production sites where it should be possible to restrict the cache clearing for some admins.
8589
                if ($this->BE_USER->getTSConfigVal('options.clearCache.all') || ($this->admin && $this->BE_USER->getTSConfigVal('options.clearCache.all') !== '0')) {
8590
                    $this->getCacheManager()->flushCaches();
8591
                    GeneralUtility::makeInstance(ConnectionPool::class)
8592
                        ->getConnectionForTable('cache_treelist')
8593
                        ->truncate('cache_treelist');
8594
8595
                    // Delete Opcode Cache
8596
                    GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
8597
                }
8598
                break;
8599
            case 'temp_cached':
8600
            case 'system':
8601
                trigger_error(
8602
                    'Calling clear_cacheCmd() with arguments "temp_cached" or "system", using'
8603
                    . ' the TSconfig option "options.clearCache.system" will be removed in TYPO3 v10, use "all"'
8604
                    . ' instead or call the group cache clearing of "system" group directly via a custom extension.',
8605
                    E_USER_DEPRECATED
8606
                );
8607
                if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.system')) {
8608
                    $this->getCacheManager()->flushCachesInGroup('system');
8609
                }
8610
                break;
8611
        }
8612
8613
        $tagsToFlush = [];
8614
        // Clear cache for a page ID!
8615
        if (MathUtility::canBeInterpretedAsInteger($cacheCmd)) {
8616
            $list_cache = [$cacheCmd];
8617
            // Call pre-processing function for clearing of cache for page ids:
8618
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8619
                $_params = ['pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()'];
8620
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8621
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8622
            }
8623
            // Delete cache for selected pages:
8624
            if (is_array($list_cache)) {
8625
                foreach ($list_cache as $pageId) {
8626
                    $tagsToFlush[] = 'pageId_' . (int)$pageId;
8627
                }
8628
            }
8629
        }
8630
        // flush cache by tag
8631
        if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
8632
            $cacheTag = substr($cacheCmd, 9);
8633
            $tagsToFlush[] = $cacheTag;
8634
        }
8635
        // process caching framwork operations
8636
        if (!empty($tagsToFlush)) {
8637
            $this->getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);
8638
        }
8639
8640
        // Call post processing function for clear-cache:
8641
        $_params = ['cacheCmd' => strtolower($cacheCmd)];
8642
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8643
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8644
        }
8645
    }
8646
8647
    /*****************************
8648
     *
8649
     * Logging
8650
     *
8651
     *****************************/
8652
    /**
8653
     * Logging actions from DataHandler
8654
     *
8655
     * @param string $table Table name the log entry is concerned with. Blank if NA
8656
     * @param int $recuid Record UID. Zero if NA
8657
     * @param int $action Action number: 0=No category, 1=new record, 2=update record, 3= delete record, 4= move record, 5= Check/evaluate
8658
     * @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
8659
     * @param int $error The severity: 0 = message, 1 = error, 2 = System Error, 3 = security notice (admin)
8660
     * @param string $details Default error message in english
8661
     * @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
8662
     * @param array $data Array with special information that may go into $details by '%s' marks / sprintf() when the log is shown
8663
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
8664
     * @param string $NEWid NEW id for new records
8665
     * @return int Log entry UID (0 if no log entry was written or logging is disabled)
8666
     */
8667
    public function log($table, $recuid, $action, $recpid, $error, $details, $details_nr = -1, $data = [], $event_pid = -1, $NEWid = '')
8668
    {
8669
        if (!$this->enableLogging) {
8670
            return 0;
8671
        }
8672
        // Type value for tce_db.php
8673
        $type = 1;
8674
        if (!$this->storeLogMessages) {
8675
            $details = '';
8676
        }
8677
        if ($error > 0) {
8678
            $detailMessage = $details;
8679
            if (is_array($data)) {
8680
                $detailMessage = vsprintf($details, $data);
8681
            }
8682
            $this->errorLog[] = '[' . $type . '.' . $action . '.' . $details_nr . ']: ' . $detailMessage;
8683
        }
8684
        return $this->BE_USER->writelog($type, $action, $error, $details_nr, $details, $data, $table, $recuid, $recpid, $event_pid, $NEWid);
8685
    }
8686
8687
    /**
8688
     * Simple logging function meant to be used when logging messages is not yet fixed.
8689
     *
8690
     * @param string $message Message string
8691
     * @param int $error Error code, see log()
8692
     * @return int Log entry UID
8693
     * @see log()
8694
     */
8695
    public function newlog($message, $error = 0)
8696
    {
8697
        return $this->log('', 0, 0, 0, $error, $message, -1);
8698
    }
8699
8700
    /**
8701
     * Simple logging function meant to bridge the gap between newlog() and log() with a little more info, in particular the record table/uid and event_pid so we can filter messages per page.
8702
     *
8703
     * @param string $message Message string
8704
     * @param string $table Table name
8705
     * @param int $uid Record uid
8706
     * @param int $pid Record PID (from page tree). Will be turned into an event_pid internally in function: Meaning that the PID for a page will be its own UID, not its page tree PID.
8707
     * @param int $error Error code, see log()
8708
     * @return int Log entry UID
8709
     * @see log()
8710
     * @deprecated since TYPO3 v9 will be removed in TYPO3 v10.0, use DataHandler->log() directly instead.
8711
     */
8712
    public function newlog2($message, $table, $uid, $pid = null, $error = 0)
8713
    {
8714
        trigger_error('DataHandler->newlog2() will be removed in TYPO3 v10.0, use the generic log() function instead.', E_USER_DEPRECATED);
8715
        if (!$this->enableLogging) {
8716
            return 0;
8717
        }
8718
        if ($pid === null) {
8719
            $propArr = $this->getRecordProperties($table, $uid);
8720
            $pid = $propArr['pid'];
8721
        }
8722
        return $this->log($table, $uid, 0, 0, $error, $message, -1, [], $this->eventPid($table, $uid, $pid));
8723
    }
8724
8725
    /**
8726
     * Print log error messages from the operations of this script instance
8727
     */
8728
    public function printLogErrorMessages()
8729
    {
8730
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
8731
        $queryBuilder->getRestrictions()->removeAll();
8732
        $result = $queryBuilder
8733
            ->select('*')
8734
            ->from('sys_log')
8735
            ->where(
8736
                $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
8737
                $queryBuilder->expr()->lt('action', $queryBuilder->createNamedParameter(256, \PDO::PARAM_INT)),
8738
                $queryBuilder->expr()->eq(
8739
                    'userid',
8740
                    $queryBuilder->createNamedParameter($this->BE_USER->user['uid'], \PDO::PARAM_INT)
8741
                ),
8742
                $queryBuilder->expr()->eq(
8743
                    'tstamp',
8744
                    $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
8745
                ),
8746
                $queryBuilder->expr()->neq('error', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8747
            )
8748
            ->execute();
8749
8750
        while ($row = $result->fetch()) {
8751
            $log_data = unserialize($row['log_data']);
8752
            $msg = $row['error'] . ': ' . sprintf($row['details'], $log_data[0], $log_data[1], $log_data[2], $log_data[3], $log_data[4]);
8753
            /** @var FlashMessage $flashMessage */
8754
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', FlashMessage::ERROR, true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

8754
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', FlashMessage::ERROR, /** @scrutinizer ignore-type */ true);
Loading history...
Bug introduced by
TYPO3\CMS\Core\Messaging\FlashMessage::ERROR of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

8754
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', /** @scrutinizer ignore-type */ FlashMessage::ERROR, true);
Loading history...
Bug introduced by
$msg of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

8754
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, /** @scrutinizer ignore-type */ $msg, '', FlashMessage::ERROR, true);
Loading history...
8755
            /** @var $flashMessageService FlashMessageService */
8756
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
8757
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
8758
            $defaultFlashMessageQueue->enqueue($flashMessage);
8759
        }
8760
    }
8761
8762
    /*****************************
8763
     *
8764
     * Internal (do not use outside Core!)
8765
     *
8766
     *****************************/
8767
8768
    /**
8769
     * Find out if the record is a get the original page
8770
     *
8771
     * @param int $pageId the page UID (can be the default page record, or a page translation record ID)
8772
     * @return int the page UID of the default page record
8773
     */
8774
    protected function getDefaultLanguagePageId(int $pageId): int
8775
    {
8776
        $localizationParentFieldName = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
8777
        $row = $this->recordInfo('pages', $pageId, $localizationParentFieldName);
8778
        $localizationParent = (int)$row[$localizationParentFieldName];
8779
        if ($localizationParent > 0) {
8780
            return $localizationParent;
8781
        }
8782
        return $pageId;
8783
    }
8784
8785
    /**
8786
     * Preprocesses field array based on field type. Some fields must be adjusted
8787
     * before going to database. This is done on the copy of the field array because
8788
     * original values are used in remap action later.
8789
     *
8790
     * @param string $table	Table name
8791
     * @param array $fieldArray	Field array to check
8792
     * @return array Updated field array
8793
     */
8794
    public function insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
8795
    {
8796
        $result = $fieldArray;
8797
        foreach ($fieldArray as $field => $value) {
8798
            if (!MathUtility::canBeInterpretedAsInteger($value)
8799
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'inline'
8800
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_field']) {
8801
                $result[$field] = count(GeneralUtility::trimExplode(',', $value, true));
8802
            }
8803
        }
8804
        return $result;
8805
    }
8806
8807
    /**
8808
     * Determines whether a particular record has been deleted
8809
     * using DataHandler::deleteRecord() in this instance.
8810
     *
8811
     * @param string $tableName
8812
     * @param string $uid
8813
     * @return bool
8814
     */
8815
    public function hasDeletedRecord($tableName, $uid)
8816
    {
8817
        return
8818
            !empty($this->deletedRecords[$tableName])
8819
            && in_array($uid, $this->deletedRecords[$tableName])
8820
        ;
8821
    }
8822
8823
    /**
8824
     * Gets the automatically versionized id of a record.
8825
     *
8826
     * @param string $table Name of the table
8827
     * @param int $id Uid of the record
8828
     * @return int
8829
     */
8830
    public function getAutoVersionId($table, $id)
8831
    {
8832
        $result = null;
8833
        if (isset($this->autoVersionIdMap[$table][$id])) {
8834
            $result = $this->autoVersionIdMap[$table][$id];
8835
        }
8836
        return $result;
8837
    }
8838
8839
    /**
8840
     * Overlays the automatically versionized id of a record.
8841
     *
8842
     * @param string $table Name of the table
8843
     * @param int $id Uid of the record
8844
     * @return int
8845
     */
8846
    protected function overlayAutoVersionId($table, $id)
8847
    {
8848
        $autoVersionId = $this->getAutoVersionId($table, $id);
8849
        if ($autoVersionId !== null) {
8850
            $id = $autoVersionId;
8851
        }
8852
        return $id;
8853
    }
8854
8855
    /**
8856
     * Adds new values to the remapStackChildIds array.
8857
     *
8858
     * @param array $idValues uid values
8859
     */
8860
    protected function addNewValuesToRemapStackChildIds(array $idValues)
8861
    {
8862
        foreach ($idValues as $idValue) {
8863
            if (strpos($idValue, 'NEW') === 0) {
8864
                $this->remapStackChildIds[$idValue] = true;
8865
            }
8866
        }
8867
    }
8868
8869
    /**
8870
     * Resolves versioned records for the current workspace scope.
8871
     * Delete placeholders and move placeholders are substituted and removed.
8872
     *
8873
     * @param string $tableName Name of the table to be processed
8874
     * @param string $fieldNames List of the field names to be fetched
8875
     * @param string $sortingField Name of the sorting field to be used
8876
     * @param array $liveIds Flat array of (live) record ids
8877
     * @return array
8878
     */
8879
    protected function resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
8880
    {
8881
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
8882
            ->getConnectionForTable($tableName);
8883
        $sortingStatement = !empty($sortingField)
8884
            ? [$connection->quoteIdentifier($sortingField)]
8885
            : null;
8886
        /** @var PlainDataResolver $resolver */
8887
        $resolver = GeneralUtility::makeInstance(
8888
            PlainDataResolver::class,
8889
            $tableName,
0 ignored issues
show
Bug introduced by
$tableName of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

8889
            /** @scrutinizer ignore-type */ $tableName,
Loading history...
8890
            $liveIds,
8891
            $sortingStatement
8892
        );
8893
8894
        $resolver->setWorkspaceId($this->BE_USER->workspace);
8895
        $resolver->setKeepDeletePlaceholder(false);
8896
        $resolver->setKeepMovePlaceholder(false);
8897
        $resolver->setKeepLiveIds(true);
8898
        $recordIds = $resolver->get();
8899
8900
        $records = [];
8901
        foreach ($recordIds as $recordId) {
8902
            $records[$recordId] = BackendUtility::getRecord($tableName, $recordId, $fieldNames);
0 ignored issues
show
Bug introduced by
$tableName of type array is incompatible with the type string expected by parameter $table 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

8902
            $records[$recordId] = BackendUtility::getRecord(/** @scrutinizer ignore-type */ $tableName, $recordId, $fieldNames);
Loading history...
8903
        }
8904
8905
        return $records;
8906
    }
8907
8908
    /**
8909
     * Gets the outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler
8910
     * Since \TYPO3\CMS\Core\DataHandling\DataHandler can create nested objects of itself,
8911
     * this method helps to determine the first (= outer most) one.
8912
     *
8913
     * @return DataHandler
8914
     */
8915
    protected function getOuterMostInstance()
8916
    {
8917
        if (!isset($this->outerMostInstance)) {
8918
            $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
8919
            foreach ($stack as $stackItem) {
8920
                if (isset($stackItem['object']) && $stackItem['object'] instanceof self) {
8921
                    $this->outerMostInstance = $stackItem['object'];
8922
                    break;
8923
                }
8924
            }
8925
        }
8926
        return $this->outerMostInstance;
8927
    }
8928
8929
    /**
8930
     * Determines whether the this object is the outer most instance of itself
8931
     * Since DataHandler can create nested objects of itself,
8932
     * this method helps to determine the first (= outer most) one.
8933
     *
8934
     * @return bool
8935
     */
8936
    public function isOuterMostInstance()
8937
    {
8938
        return $this->getOuterMostInstance() === $this;
8939
    }
8940
8941
    /**
8942
     * Gets an instance of the runtime cache.
8943
     *
8944
     * @return FrontendInterface
8945
     */
8946
    protected function getRuntimeCache()
8947
    {
8948
        return $this->getCacheManager()->getCache('cache_runtime');
8949
    }
8950
8951
    /**
8952
     * Determines nested element calls.
8953
     *
8954
     * @param string $table Name of the table
8955
     * @param int $id Uid of the record
8956
     * @param string $identifier Name of the action to be checked
8957
     * @return bool
8958
     */
8959
    protected function isNestedElementCallRegistered($table, $id, $identifier)
8960
    {
8961
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8962
        return isset($nestedElementCalls[$identifier][$table][$id]);
8963
    }
8964
8965
    /**
8966
     * Registers nested elements calls.
8967
     * This is used to track nested calls (e.g. for following m:n relations).
8968
     *
8969
     * @param string $table Name of the table
8970
     * @param int $id Uid of the record
8971
     * @param string $identifier Name of the action to be tracked
8972
     */
8973
    protected function registerNestedElementCall($table, $id, $identifier)
8974
    {
8975
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8976
        $nestedElementCalls[$identifier][$table][$id] = true;
8977
        $this->runtimeCache->set($this->cachePrefixNestedElementCalls, $nestedElementCalls);
8978
    }
8979
8980
    /**
8981
     * Resets the nested element calls.
8982
     */
8983
    protected function resetNestedElementCalls()
8984
    {
8985
        $this->runtimeCache->remove($this->cachePrefixNestedElementCalls);
8986
    }
8987
8988
    /**
8989
     * Determines whether an element was registered to be deleted in the registry.
8990
     *
8991
     * @param string $table Name of the table
8992
     * @param int $id Uid of the record
8993
     * @return bool
8994
     * @see registerElementsToBeDeleted
8995
     * @see resetElementsToBeDeleted
8996
     * @see copyRecord_raw
8997
     * @see versionizeRecord
8998
     */
8999
    protected function isElementToBeDeleted($table, $id)
9000
    {
9001
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
9002
        return isset($elementsToBeDeleted[$table][$id]);
9003
    }
9004
9005
    /**
9006
     * Registers elements to be deleted in the registry.
9007
     *
9008
     * @see process_datamap
9009
     */
9010
    protected function registerElementsToBeDeleted()
9011
    {
9012
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
9013
        $this->runtimeCache->set('core-datahandler-elementsToBeDeleted', array_merge($elementsToBeDeleted, $this->getCommandMapElements('delete')));
9014
    }
9015
9016
    /**
9017
     * Resets the elements to be deleted in the registry.
9018
     *
9019
     * @see process_datamap
9020
     */
9021
    protected function resetElementsToBeDeleted()
9022
    {
9023
        $this->runtimeCache->remove('core-datahandler-elementsToBeDeleted');
9024
    }
9025
9026
    /**
9027
     * Unsets elements (e.g. of the data map) that shall be deleted.
9028
     * This avoids to modify records that will be deleted later on.
9029
     *
9030
     * @param array $elements Elements to be modified
9031
     * @return array
9032
     */
9033
    protected function unsetElementsToBeDeleted(array $elements)
9034
    {
9035
        $elements = ArrayUtility::arrayDiffAssocRecursive($elements, $this->getCommandMapElements('delete'));
9036
        foreach ($elements as $key => $value) {
9037
            if (empty($value)) {
9038
                unset($elements[$key]);
9039
            }
9040
        }
9041
        return $elements;
9042
    }
9043
9044
    /**
9045
     * Gets elements of the command map that match a particular command.
9046
     *
9047
     * @param string $needle The command to be matched
9048
     * @return array
9049
     */
9050
    protected function getCommandMapElements($needle)
9051
    {
9052
        $elements = [];
9053
        foreach ($this->cmdmap as $tableName => $idArray) {
9054
            foreach ($idArray as $id => $commandArray) {
9055
                foreach ($commandArray as $command => $value) {
9056
                    if ($value && $command == $needle) {
9057
                        $elements[$tableName][$id] = true;
9058
                    }
9059
                }
9060
            }
9061
        }
9062
        return $elements;
9063
    }
9064
9065
    /**
9066
     * Controls active elements and sets NULL values if not active.
9067
     * Datamap is modified accordant to submitted control values.
9068
     */
9069
    protected function controlActiveElements()
9070
    {
9071
        if (!empty($this->control['active'])) {
9072
            $this->setNullValues(
9073
                $this->control['active'],
9074
                $this->datamap
9075
            );
9076
        }
9077
    }
9078
9079
    /**
9080
     * Sets NULL values in haystack array.
9081
     * The general behaviour in the user interface is to enable/activate fields.
9082
     * Thus, this method uses NULL as value to be stored if a field is not active.
9083
     *
9084
     * @param array $active hierarchical array with active elements
9085
     * @param array $haystack hierarchical array with haystack to be modified
9086
     */
9087
    protected function setNullValues(array $active, array &$haystack)
9088
    {
9089
        foreach ($active as $key => $value) {
9090
            // Nested data is processes recursively
9091
            if (is_array($value)) {
9092
                $this->setNullValues(
9093
                    $value,
9094
                    $haystack[$key]
9095
                );
9096
            } elseif ($value == 0) {
9097
                // Field has not been activated in the user interface,
9098
                // thus a NULL value shall be stored in the database
9099
                $haystack[$key] = null;
9100
            }
9101
        }
9102
    }
9103
9104
    /**
9105
     * Entry point to post process a database insert. Currently bails early unless a UID has been forced
9106
     * and the database platform is not MySQL.
9107
     *
9108
     * @param \TYPO3\CMS\Core\Database\Connection $connection
9109
     * @param string $tableName
9110
     * @param int $suggestedUid
9111
     * @return int
9112
     */
9113
    protected function postProcessDatabaseInsert(Connection $connection, string $tableName, int $suggestedUid): int
9114
    {
9115
        if ($suggestedUid !== 0 && $connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
9116
            $this->postProcessPostgresqlInsert($connection, $tableName);
9117
            // The last inserted id on postgresql is actually the last value generated by the sequence.
9118
            // On a forced UID insert this might not be the actual value or the sequence might not even
9119
            // have generated a value yet.
9120
            // Return the actual ID we forced on insert as a surrogate.
9121
            return $suggestedUid;
9122
        }
9123
9124
        return $connection->lastInsertId($tableName);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $connection->lastInsertId($tableName) returns the type string which is incompatible with the type-hinted return integer.
Loading history...
9125
    }
9126
9127
    /**
9128
     * PostgreSQL works with sequences for auto increment columns. A sequence is not updated when a value is
9129
     * written to such a column. To avoid clashes when the sequence returns an existing ID this helper will
9130
     * update the sequence to the current max value of the column.
9131
     *
9132
     * @param \TYPO3\CMS\Core\Database\Connection $connection
9133
     * @param string $tableName
9134
     */
9135
    protected function postProcessPostgresqlInsert(Connection $connection, string $tableName)
9136
    {
9137
        $queryBuilder = $connection->createQueryBuilder();
9138
        $queryBuilder->getRestrictions()->removeAll();
9139
        $row = $queryBuilder->select('PGT.schemaname', 'S.relname', 'C.attname', 'T.relname AS tablename')
9140
            ->from('pg_class', 'S')
9141
            ->from('pg_depend', 'D')
9142
            ->from('pg_class', 'T')
9143
            ->from('pg_attribute', 'C')
9144
            ->from('pg_tables', 'PGT')
9145
            ->where(
9146
                $queryBuilder->expr()->eq('S.relkind', $queryBuilder->quote('S')),
9147
                $queryBuilder->expr()->eq('S.oid', $queryBuilder->quoteIdentifier('D.objid')),
9148
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('T.oid')),
9149
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('C.attrelid')),
9150
                $queryBuilder->expr()->eq('D.refobjsubid', $queryBuilder->quoteIdentifier('C.attnum')),
9151
                $queryBuilder->expr()->eq('T.relname', $queryBuilder->quoteIdentifier('PGT.tablename')),
9152
                $queryBuilder->expr()->eq('PGT.tablename', $queryBuilder->quote($tableName))
9153
            )
9154
            ->setMaxResults(1)
9155
            ->execute()
9156
            ->fetch();
9157
9158
        if ($row !== false) {
9159
            $connection->exec(
9160
                sprintf(
9161
                    'SELECT SETVAL(%s, COALESCE(MAX(%s), 0)+1, FALSE) FROM %s',
9162
                    $connection->quote($row['schemaname'] . '.' . $row['relname']),
9163
                    $connection->quoteIdentifier($row['attname']),
9164
                    $connection->quoteIdentifier($row['schemaname'] . '.' . $row['tablename'])
9165
                )
9166
            );
9167
        }
9168
    }
9169
9170
    /**
9171
     * Return the cache entry identifier for field evals
9172
     *
9173
     * @param string $additionalIdentifier
9174
     * @return string
9175
     */
9176
    protected function getFieldEvalCacheIdentifier($additionalIdentifier)
9177
    {
9178
        return 'core-datahandler-eval-' . md5($additionalIdentifier);
9179
    }
9180
9181
    /**
9182
     * @return RelationHandler
9183
     */
9184
    protected function createRelationHandlerInstance()
9185
    {
9186
        $isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces');
9187
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
9188
        $relationHandler->setWorkspaceId($this->BE_USER->workspace);
9189
        $relationHandler->setUseLiveReferenceIds($isWorkspacesLoaded);
9190
        $relationHandler->setUseLiveParentIds($isWorkspacesLoaded);
9191
        return $relationHandler;
9192
    }
9193
9194
    /**
9195
     * Create and returns an instance of the CacheManager
9196
     *
9197
     * @return CacheManager
9198
     */
9199
    protected function getCacheManager()
9200
    {
9201
        return GeneralUtility::makeInstance(CacheManager::class);
9202
    }
9203
9204
    /**
9205
     * Gets the resourceFactory
9206
     *
9207
     * @return ResourceFactory
9208
     */
9209
    protected function getResourceFactory()
9210
    {
9211
        return ResourceFactory::getInstance();
9212
    }
9213
9214
    /**
9215
     * @return LanguageService
9216
     */
9217
    protected function getLanguageService()
9218
    {
9219
        return $GLOBALS['LANG'];
9220
    }
9221
}
9222