Test Failed
Branch master (137376)
by Tymoteusz
20:39
created

DataHandler::triggerRemapAction()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 4
nc 2
nop 5
dl 0
loc 7
rs 9.2
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\VariableFrontend;
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\VariableFrontend
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 View Code Duplication
        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 View Code Duplication
    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 View Code Duplication
        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 View Code Duplication
            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 View Code Duplication
                    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 View Code Duplication
                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 $table
1306
     * @param $row
1307
     * @param $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 View Code Duplication
                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 isset($transformedLabel['value']) ? $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 (strstr($id, 'NEW')) {
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 < pow(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 = isset($GLOBALS['PAGES_TYPES'][$value]['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 View Code Duplication
                    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 View Code Duplication
            if (isset($tcaFieldConf['range']['upper']) && (int)$value > (int)$tcaFieldConf['range']['upper']) {
1860
                $value = $tcaFieldConf['range']['upper'];
1861
            }
1862 View Code Duplication
            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 View Code Duplication
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
1888
                    $res['value'] = $this->getUnique($table, $field, $res['value'], $id, $realPid);
1889
                }
1890 View Code Duplication
                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 = pow(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 View Code Duplication
            if (in_array('maximumRecordsCheckedInPid', $evalCodesArray, true)) {
1957
                $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value, $realPid);
1958
                $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsCheckedInPid'];
1959
            }
1960 View Code Duplication
            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 $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 View Code Duplication
                    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 && !strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
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 View Code Duplication
                    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 (strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
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 = isset($this->alternativeFileName[$theFile]) ? $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 View Code Duplication
                                        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 View Code Duplication
                                } else {
2296
                                    $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2297
                                }
2298 View Code Duplication
                            } 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 View Code Duplication
                    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 View Code Duplication
                } 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 View Code Duplication
                        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 = isset($this->alternativeFileName[$theFile]) ? $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 View Code Duplication
                                            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 View Code Duplication
                                    } else {
2389
                                        $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2390
                                    }
2391 View Code Duplication
                                } 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 = $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 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\Statement 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 View Code Duplication
        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 View Code Duplication
                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, [',' => '.', '-' => '']);
0 ignored issues
show
Bug introduced by
The call to strtr() has too few arguments starting with to. ( Ignorable by Annotation )

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

2866
                    $value = /** @scrutinizer ignore-call */ strtr($value, [',' => '.', '-' => '']);

This check compares calls to functions or methods with their respective definitions. If the call has less 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...
Bug introduced by
array(',' => '.', '-' => '') of type array<string,string> is incompatible with the type string expected by parameter $from of strtr(). ( Ignorable by Annotation )

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

2866
                    $value = strtr($value, /** @scrutinizer ignore-type */ [',' => '.', '-' => '']);
Loading history...
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 View Code Duplication
                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 View Code Duplication
                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 View Code Duplication
            } 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(',', '', 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 View Code Duplication
        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 View Code Duplication
            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 View Code Duplication
                    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 View Code Duplication
                    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 View Code Duplication
                    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 View Code Duplication
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3503
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3504
        }
3505
        // Setting original UID:
3506 View Code Duplication
        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);
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type array expected by parameter $cmd of TYPO3\CMS\Core\DataHandling\DataHandler::start(). ( Ignorable by Annotation )

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

3512
        $copyTCE->start($data, /** @scrutinizer ignore-type */ '', $this->BE_USER);
Loading history...
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 View Code Duplication
                        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 View Code Duplication
                    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 View Code Duplication
                    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 View Code Duplication
        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);
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
     * @param string $inlineSubType
3964
     * @return mixed
3965
     */
3966
    protected function copyRecord_processInline(
3967
        $table,
3968
        $uid,
3969
        $field,
3970
        $value,
3971
        $row,
3972
        $conf,
3973
        $realDestPid,
3974
        $language,
3975
        array $workspaceOptions,
3976
        $inlineSubType
3977
    ) {
3978
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3979
        /** @var $dbAnalysis RelationHandler */
3980
        $dbAnalysis = $this->createRelationHandlerInstance();
3981
        $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3982
        // Walk through the items, copy them and remember the new id:
3983
        foreach ($dbAnalysis->itemArray as $k => $v) {
3984
            $newId = null;
3985
            // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3986
            if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3987
                // Children should be localized when the parent gets localized the first time, just do it:
3988
                $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

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

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

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

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

4469
                list($parentUid) = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4470
                $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4471
                // Setting PID
4472
                $updateFields['pid'] = $destPid;
4473
                // Table is sorted by 'sortby'
4474
                if ($sortRow && !isset($updateFields[$sortRow])) {
4475
                    $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4476
                    $updateFields[$sortRow] = $sortNumber;
4477
                }
4478
                // Check for child records that have also to be moved
4479
                $this->moveRecord_procFields($table, $uid, $destPid);
4480
                // Create query for update:
4481
                GeneralUtility::makeInstance(ConnectionPool::class)
4482
                    ->getConnectionForTable($table)
4483
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4484
                // Check for the localizations of that element
4485
                $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4486
                // Call post processing hooks:
4487 View Code Duplication
                foreach ($hookObjectsArr as $hookObj) {
4488
                    if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4489
                        $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4490
                    }
4491
                }
4492
4493
                $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields]);
4494
                if ($this->enableLogging) {
4495
                    // Logging...
4496
                    $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4497
                    if ($destPid != $propArr['pid']) {
4498
                        // Logged to old page
4499
                        $newPropArr = $this->getRecordProperties($table, $uid);
4500
                        $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4501
                        $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']);
4502
                        // Logged to new page
4503
                        $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);
4504 View Code Duplication
                    } else {
4505
                        // Logged to new page
4506
                        $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);
4507
                    }
4508
                }
4509
                // Clear cache after moving
4510
                $this->registerRecordIdForPageCacheClearing($table, $uid);
4511
                $this->fixUniqueInPid($table, $uid);
4512
                // fixCopyAfterDuplFields
4513
                if ($origDestPid < 0) {
4514
                    $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

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

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

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

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

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

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

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

5348
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, /** @scrutinizer ignore-type */ 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

5348
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, /** @scrutinizer ignore-type */ $res, '', FlashMessage::ERROR, 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

5348
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', /** @scrutinizer ignore-type */ FlashMessage::ERROR, true);
Loading history...
5349
            /** @var $flashMessageService FlashMessageService */
5350
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
5351
            $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
5352
            $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

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

5531
                $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...
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

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

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

5582
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
5583
                    $dbAnalysis->undeleteRecord = true;
5584
5585
                    $enableCascadingDelete = true;
5586
                    // non type save comparison is intended!
5587
                    if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5588
                        $enableCascadingDelete = false;
5589
                    }
5590
5591
                    // Walk through the items and remove them
5592
                    foreach ($dbAnalysis->itemArray as $v) {
5593
                        if (!$undeleteRecord) {
5594
                            if ($enableCascadingDelete) {
5595
                                $this->deleteAction($v['table'], $v['id']);
5596
                            }
5597
                        } else {
5598
                            $this->undeleteRecord($v['table'], $v['id']);
5599
                        }
5600
                    }
5601
                }
5602
            }
5603
        } elseif ($this->isReferenceField($conf)) {
5604
            $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5605
            $dbAnalysis = $this->createRelationHandlerInstance();
5606
            $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5607
            foreach ($dbAnalysis->itemArray as $v) {
5608
                $this->updateRefIndexStack[$table][$uid][] = [$v['table'], $v['id']];
5609
            }
5610
        }
5611
    }
5612
5613
    /**
5614
     * Find l10n-overlay records and perform the requested delete action for these records.
5615
     *
5616
     * @param string $table Record Table
5617
     * @param string $uid Record UID
5618
     */
5619
    public function deleteL10nOverlayRecords($table, $uid)
5620
    {
5621
        // Check whether table can be localized
5622
        if (!BackendUtility::isTableLocalizable($table)) {
5623
            return;
5624
        }
5625
5626
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5627
        $queryBuilder->getRestrictions()
5628
            ->removeAll()
5629
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5630
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
5631
5632
        $queryBuilder->select('*')
5633
            ->from($table)
5634
            ->where(
5635
                $queryBuilder->expr()->eq(
5636
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5637
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5638
                )
5639
            );
5640
5641 View Code Duplication
        if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
5642
            $queryBuilder->andWhere(
5643
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
5644
            );
5645
        }
5646
5647
        $result = $queryBuilder->execute();
5648
        while ($record = $result->fetch()) {
5649
            // Ignore workspace delete placeholders. Those records have been marked for
5650
            // deletion before - deleting them again in a workspace would revert that state.
5651
            if ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5652
                BackendUtility::workspaceOL($table, $record);
5653
                if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5654
                    continue;
5655
                }
5656
            }
5657
            $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5658
        }
5659
    }
5660
5661
    /*********************************************
5662
     *
5663
     * Cmd: Versioning
5664
     *
5665
     ********************************************/
5666
    /**
5667
     * Creates a new version of a record
5668
     * (Requires support in the table)
5669
     *
5670
     * @param string $table Table name
5671
     * @param int $id Record uid to versionize
5672
     * @param string $label Version label
5673
     * @param bool $delete If TRUE, the version is created to delete the record.
5674
     * @return int|null Returns the id of the new version (if any)
5675
     * @see copyRecord()
5676
     */
5677
    public function versionizeRecord($table, $id, $label, $delete = false)
5678
    {
5679
        $id = (int)$id;
5680
        // Stop any actions if the record is marked to be deleted:
5681
        // (this can occur if IRRE elements are versionized and child elements are removed)
5682
        if ($this->isElementToBeDeleted($table, $id)) {
5683
            return null;
5684
        }
5685
        if (!$GLOBALS['TCA'][$table] || !$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] || $id <= 0) {
5686
            $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, 1);
5687
            return null;
5688
        }
5689
5690
        // Fetch record with permission check
5691
        $row = $this->recordInfoWithPermissionCheck($table, $id, 'show');
5692
5693
        // This checks if the record can be selected which is all that a copy action requires.
5694 View Code Duplication
        if ($row === false) {
5695
            $this->newlog(
5696
                'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "' . $table . ':' . $id . '"',
5697
                1
5698
            );
5699
            return null;
5700
        }
5701
5702
        // Record must be online record
5703 View Code Duplication
        if ($row['pid'] < 0) {
5704
            $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (pid=-1)!', 1);
5705
            return null;
5706
        }
5707
5708
        // Record must not be placeholder for moving.
5709
        if (VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
5710
            $this->newlog('Record cannot be versioned because it is a placeholder for a moving operation', 1);
5711
            return null;
5712
        }
5713
5714
        if ($delete && $this->cannotDeleteRecord($table, $id)) {
5715
            $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), 1);
5716
            return null;
5717
        }
5718
5719
        // Look for next version number:
5720
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5721
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5722
        $highestVerNumber = $queryBuilder
5723
            ->select('t3ver_id')
5724
            ->from($table)
5725
            ->where($queryBuilder->expr()->orX(
5726
                $queryBuilder->expr()->andX(
5727
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
5728
                    $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
5729
                ),
5730
                $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
5731
            ))
5732
            ->orderBy('t3ver_id', 'DESC')
5733
            ->setMaxResults(1)
5734
            ->execute()
5735
            ->fetchColumn(0);
5736
        // Look for version number of the current:
5737
        $subVer = $row['t3ver_id'] . '.' . ($highestVerNumber + 1);
5738
        // Set up the values to override when making a raw-copy:
5739
        $overrideArray = [
5740
            't3ver_id' => $highestVerNumber + 1,
5741
            't3ver_oid' => $id,
5742
            't3ver_label' => $label ?: $subVer . ' / ' . date('d-m-Y H:m:s'),
0 ignored issues
show
Bug introduced by
Are you sure date('d-m-Y H:m:s') of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

5742
            't3ver_label' => $label ?: $subVer . ' / ' . /** @scrutinizer ignore-type */ date('d-m-Y H:m:s'),
Loading history...
5743
            't3ver_wsid' => $this->BE_USER->workspace,
5744
            't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5745
            't3ver_count' => 0,
5746
            't3ver_stage' => 0,
5747
            't3ver_tstamp' => 0
5748
        ];
5749 View Code Duplication
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5750
            $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5751
        }
5752
        // Checking if the record already has a version in the current workspace of the backend user
5753
        if ($this->BE_USER->workspace !== 0) {
5754
            // Look for version already in workspace:
5755
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5756
        }
5757
        // Create new version of the record and return the new uid
5758
        if (empty($versionRecord['uid'])) {
5759
            // Create raw-copy and return result:
5760
            // The information of the label to be used for the workspace record
5761
            // as well as the information whether the record shall be removed
5762
            // must be forwarded (creating remove placeholders on a workspace are
5763
            // done by copying the record and override several fields).
5764
            $workspaceOptions = [
5765
                'delete' => $delete,
5766
                'label' => $label,
5767
            ];
5768
            return $this->copyRecord_raw($table, $id, -1, $overrideArray, $workspaceOptions);
5769
        }
5770
        // Reuse the existing record and return its uid
5771
        // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5772
        // did not make much sense since the information is available)
5773
        return $versionRecord['uid'];
5774
    }
5775
5776
    /**
5777
     * Swaps MM-relations for current/swap record, see version_swap()
5778
     *
5779
     * @param string $table Table for the two input records
5780
     * @param int $id Current record (about to go offline)
5781
     * @param int $swapWith Swap record (about to go online)
5782
     * @see version_swap()
5783
     */
5784
    public function version_remapMMForVersionSwap($table, $id, $swapWith)
5785
    {
5786
        // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5787
        $currentRec = BackendUtility::getRecord($table, $id);
5788
        $swapRec = BackendUtility::getRecord($table, $swapWith);
5789
        $this->version_remapMMForVersionSwap_reg = [];
5790
        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5791
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5792
            $conf = $fConf['config'];
5793
            if ($this->isReferenceField($conf)) {
5794
                $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5795
                $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5796
                if ($conf['MM']) {
5797
                    /** @var $dbAnalysis RelationHandler */
5798
                    $dbAnalysis = $this->createRelationHandlerInstance();
5799
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5800 View Code Duplication
                    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

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

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

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

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

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

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

7277
                    $this->log($table, $id, /** @scrutinizer ignore-type */ $action, 0, 1, $message);
Loading history...
7278
                }
7279
                // Return selected rows:
7280
                return $row;
7281
            }
7282
        }
7283
        return null;
7284
    }
7285
7286
    /**
7287
     * Setting sys_history record, based on content previously set in $this->historyRecords[$table . ':' . $id] (by compareFieldArrayWithCurrentAndUnset())
7288
     *
7289
     * This functionality is now moved into the RecordHistoryStore and can be used instead.
7290
     *
7291
     * @param string $table Table name
7292
     * @param int $id Record ID
7293
     * @param int $logId Log entry ID, important for linking between log and history views
7294
     */
7295
    public function setHistory($table, $id, $logId)
7296
    {
7297 View Code Duplication
        if (isset($this->historyRecords[$table . ':' . $id])) {
7298
            $this->getRecordHistoryStore()->modifyRecord(
7299
                $table,
7300
                $id,
7301
                $this->historyRecords[$table . ':' . $id]
7302
            );
7303
        }
7304
    }
7305
7306
    /**
7307
     * @return RecordHistoryStore
7308
     */
7309
    protected function getRecordHistoryStore(): RecordHistoryStore
7310
    {
7311
        return GeneralUtility::makeInstance(
7312
            RecordHistoryStore::class,
7313
            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

7313
            /** @scrutinizer ignore-type */ RecordHistoryStore::USER_BACKEND,
Loading history...
7314
            $this->BE_USER->user['uid'],
7315
            $this->BE_USER->user['ses_backuserid'] ?? null,
7316
            $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

7316
            /** @scrutinizer ignore-type */ $this->BE_USER->workspace
Loading history...
7317
        );
7318
    }
7319
7320
    /**
7321
     * Update Reference Index (sys_refindex) for a record
7322
     * Should be called any almost any update to a record which could affect references inside the record.
7323
     *
7324
     * @param string $table Table name
7325
     * @param int $id Record UID
7326
     */
7327
    public function updateRefIndex($table, $id)
7328
    {
7329
        /** @var $refIndexObj ReferenceIndex */
7330
        $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
7331
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
7332
            $refIndexObj->setWorkspaceId($this->BE_USER->workspace);
7333
        }
7334
        $refIndexObj->enableRuntimeCache();
7335
        $refIndexObj->updateRefIndexTable($table, $id);
7336
    }
7337
7338
    /*********************************************
7339
     *
7340
     * Misc functions
7341
     *
7342
     ********************************************/
7343
    /**
7344
     * Returning sorting number for tables with a "sortby" column
7345
     * Using when new records are created and existing records are moved around.
7346
     *
7347
     * @param string $table Table name
7348
     * @param int $uid Uid of record to find sorting number for. May be zero in case of new.
7349
     * @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)
7350
     * @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.
7351
     */
7352
    public function getSortNumber($table, $uid, $pid)
7353
    {
7354
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
7355
            $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
7356
            $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
7357
            $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7358
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7359
7360
            $queryBuilder
7361
                ->select($sortRow, 'pid', 'uid')
7362
                ->from($table);
7363
7364
            // Sorting number is in the top
7365
            if ($pid >= 0) {
7366
                // Fetches the first record under this pid
7367
                $row = $queryBuilder
7368
                    ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7369
                    ->orderBy($sortRow, 'ASC')
7370
                    ->setMaxResults(1)
7371
                    ->execute()
7372
                    ->fetch();
7373
                // There was an element
7374
                if (!empty($row)) {
7375
                    // The top record was the record it self, so we return its current sortnumber
7376
                    if ($row['uid'] == $uid) {
7377
                        return $row[$sortRow];
7378
                    }
7379
                    // If the pages sortingnumber < 1 we must resort the records under this pid
7380
                    if ($row[$sortRow] < 1) {
7381
                        $this->resorting($table, $pid, $sortRow, 0);
7382
                        // First sorting number after resorting
7383
                        return $this->sortIntervals;
7384
                    }
7385
                    // Sorting number between current top element and zero
7386
                    return floor($row[$sortRow] / 2);
7387
                }
7388
                // No pages, so we choose the default value as sorting-number
7389
                // First sorting number if no elements.
7390
                return $this->sortIntervals;
7391
            }
7392
            // Sorting number is inside the list
7393
            // Fetches the record which is supposed to be the prev record
7394
            $row = $queryBuilder
7395
                    ->where($queryBuilder->expr()->eq(
7396
                        'uid',
7397
                        $queryBuilder->createNamedParameter(abs($pid), \PDO::PARAM_INT)
7398
                    ))
7399
                    ->execute()
7400
                    ->fetch();
7401
7402
            // There was a record
7403
            if (!empty($row)) {
7404
                // 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.
7405
                if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortRow . ',pid,uid')) {
7406
                    $row = $lookForLiveVersion;
7407
                }
7408
                // Fetch move placeholder, since it might point to a new page in the current workspace
7409
                if ($movePlaceholder = BackendUtility::getMovePlaceholder($table, $row['uid'], 'uid,pid,' . $sortRow)) {
7410
                    $row = $movePlaceholder;
7411
                }
7412
                // If the record should be inserted after itself, keep the current sorting information:
7413
                if ((int)$row['uid'] === (int)$uid) {
7414
                    $sortNumber = $row[$sortRow];
7415
                } else {
7416
                    $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7417
                    $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7418
7419
                    $subResults = $queryBuilder
7420
                            ->select($sortRow, 'pid', 'uid')
7421
                            ->from($table)
7422
                            ->where(
7423
                                $queryBuilder->expr()->eq(
7424
                                    'pid',
7425
                                    $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT)
7426
                                ),
7427
                                $queryBuilder->expr()->gte(
7428
                                    $sortRow,
7429
                                    $queryBuilder->createNamedParameter($row[$sortRow], \PDO::PARAM_INT)
7430
                                )
7431
                            )
7432
                            ->orderBy($sortRow, 'ASC')
7433
                            ->setMaxResults(2)
7434
                            ->execute()
7435
                            ->fetchAll();
7436
                    // Fetches the next record in order to calculate the in-between sortNumber
7437
                    // There was a record afterwards
7438
                    if (count($subResults) === 2) {
7439
                        // There was a record afterwards, fetch that
7440
                        $subrow = array_pop($subResults);
7441
                        // The sortNumber is found in between these values
7442
                        $sortNumber = $row[$sortRow] + floor(($subrow[$sortRow] - $row[$sortRow]) / 2);
7443
                        // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
7444
                        if ($sortNumber <= $row[$sortRow] || $sortNumber >= $subrow[$sortRow]) {
7445
                            // By this special param, resorting reserves and returns the sortnumber after the uid
7446
                            $sortNumber = $this->resorting($table, $row['pid'], $sortRow, $row['uid']);
7447
                        }
7448
                    } else {
7449
                        // If after the last record in the list, we just add the sortInterval to the last sortvalue
7450
                        $sortNumber = $row[$sortRow] + $this->sortIntervals;
7451
                    }
7452
                }
7453
                return ['pid' => $row['pid'], 'sortNumber' => $sortNumber];
7454
            }
7455 View Code Duplication
            if ($this->enableLogging) {
7456
                $propArr = $this->getRecordProperties($table, $uid);
7457
                // OK, don't insert $propArr['event_pid'] here...
7458
                $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']);
7459
            }
7460
            // There MUST be a page or else this cannot work
7461
            return false;
7462
        }
7463
        return null;
7464
    }
7465
7466
    /**
7467
     * Resorts a table.
7468
     * Used internally by getSortNumber()
7469
     *
7470
     * @param string $table Table name
7471
     * @param int $pid Pid in which to resort records.
7472
     * @param string $sortRow Sorting row
7473
     * @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
7474
     * @return int|null If $return_SortNumber_After_This_Uid is set, will contain usable sorting number after that record if found (otherwise 0)
7475
     * @access private
7476
     * @see getSortNumber()
7477
     */
7478
    public function resorting($table, $pid, $sortRow, $return_SortNumber_After_This_Uid)
7479
    {
7480
        if ($GLOBALS['TCA'][$table] && $sortRow && $GLOBALS['TCA'][$table]['ctrl']['sortby'] == $sortRow) {
7481
            $returnVal = 0;
7482
            $intervals = $this->sortIntervals;
7483
            $i = $intervals * 2;
7484
            $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7485
            $queryBuilder = $connection->createQueryBuilder();
7486
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7487
7488
            $result = $queryBuilder
7489
                ->select('uid')
7490
                ->from($table)
7491
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7492
                ->orderBy($sortRow, 'ASC')
7493
                ->addOrderBy('uid', 'ASC')
7494
                ->execute();
7495
            while ($row = $result->fetch()) {
7496
                $uid = (int)$row['uid'];
7497
                if ($uid) {
7498
                    $connection->update($table, [$sortRow => $i], ['uid' => (int)$uid]);
7499
                    // This is used to return a sortingValue if the list is resorted because of inserting records inside the list and not in the top
7500
                    if ($uid == $return_SortNumber_After_This_Uid) {
7501
                        $i = $i + $intervals;
7502
                        $returnVal = $i;
7503
                    }
7504
                } else {
7505
                    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...
7506
                }
7507
                $i = $i + $intervals;
7508
            }
7509
            return $returnVal;
7510
        }
7511
        return null;
7512
    }
7513
7514
    /**
7515
     * Returning uid of previous localized record, if any, for tables with a "sortby" column
7516
     * Used when new localized records are created so that localized records are sorted in the same order as the default language records
7517
     *
7518
     * @param string $table Table name
7519
     * @param int $uid Uid of default language record
7520
     * @param int $pid Pid of default language record
7521
     * @param int $language Language of localization
7522
     * @return int uid of record after which the localized record should be inserted
7523
     */
7524
    protected function getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
7525
    {
7526
        $previousLocalizedRecordUid = $uid;
7527
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
7528
            $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
7529
            $select = [$sortRow, 'pid', 'uid'];
7530
            // For content elements, we also need the colPos
7531
            if ($table === 'tt_content') {
7532
                $select[] = 'colPos';
7533
            }
7534
            // Get the sort value of the default language record
7535
            $row = BackendUtility::getRecord($table, $uid, implode(',', $select));
7536
            if (is_array($row)) {
7537
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7538
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7539
7540
                $queryBuilder
7541
                    ->select(...$select)
7542
                    ->from($table)
7543
                    ->where(
7544
                        $queryBuilder->expr()->eq(
7545
                            'pid',
7546
                            $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
7547
                        ),
7548
                        $queryBuilder->expr()->eq(
7549
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
7550
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
7551
                        ),
7552
                        $queryBuilder->expr()->lt(
7553
                            $sortRow,
7554
                            $queryBuilder->createNamedParameter($row[$sortRow], \PDO::PARAM_INT)
7555
                        )
7556
                    )
7557
                    ->orderBy($sortRow, 'DESC')
7558
                    ->setMaxResults(1);
7559
                if ($table === 'tt_content') {
7560
                    $queryBuilder
7561
                        ->andWhere(
7562
                            $queryBuilder->expr()->eq(
7563
                                'colPos',
7564
                                $queryBuilder->createNamedParameter($row['colPos'], \PDO::PARAM_INT)
7565
                            )
7566
                        );
7567
                }
7568
                // If there is an element, find its localized record in specified localization language
7569
                if ($previousRow = $queryBuilder->execute()->fetch()) {
7570
                    $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $language);
7571
                    if (is_array($previousLocalizedRecord[0])) {
7572
                        $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
7573
                    }
7574
                }
7575
            }
7576
        }
7577
        return $previousLocalizedRecordUid;
7578
    }
7579
7580
    /**
7581
     * Setting up perms_* fields in $fieldArray based on TSconfig input
7582
     * Used for new pages
7583
     *
7584
     * @param array $fieldArray Field Array, returned with modifications
7585
     * @param array $TSConfig_p TSconfig properties
7586
     * @return array Modified Field Array
7587
     */
7588
    public function setTSconfigPermissions($fieldArray, $TSConfig_p)
7589
    {
7590
        if ((string)$TSConfig_p['userid'] !== '') {
7591
            $fieldArray['perms_userid'] = (int)$TSConfig_p['userid'];
7592
        }
7593
        if ((string)$TSConfig_p['groupid'] !== '') {
7594
            $fieldArray['perms_groupid'] = (int)$TSConfig_p['groupid'];
7595
        }
7596
        if ((string)$TSConfig_p['user'] !== '') {
7597
            $fieldArray['perms_user'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['user']) ? $TSConfig_p['user'] : $this->assemblePermissions($TSConfig_p['user']);
7598
        }
7599 View Code Duplication
        if ((string)$TSConfig_p['group'] !== '') {
7600
            $fieldArray['perms_group'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['group']) ? $TSConfig_p['group'] : $this->assemblePermissions($TSConfig_p['group']);
7601
        }
7602 View Code Duplication
        if ((string)$TSConfig_p['everybody'] !== '') {
7603
            $fieldArray['perms_everybody'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['everybody']) ? $TSConfig_p['everybody'] : $this->assemblePermissions($TSConfig_p['everybody']);
7604
        }
7605
        return $fieldArray;
7606
    }
7607
7608
    /**
7609
     * 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.
7610
     * Used for new records and during copy operations for defaults
7611
     *
7612
     * @param string $table Table name for which to set default values.
7613
     * @return array Array with default values.
7614
     */
7615
    public function newFieldArray($table)
7616
    {
7617
        $fieldArray = [];
7618
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
7619
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $content) {
7620 View Code Duplication
                if (isset($this->defaultValues[$table][$field])) {
7621
                    $fieldArray[$field] = $this->defaultValues[$table][$field];
7622
                } elseif (isset($content['config']['default'])) {
7623
                    $fieldArray[$field] = $content['config']['default'];
7624
                }
7625
            }
7626
        }
7627
        // Set default permissions for a page.
7628
        if ($table === 'pages') {
7629
            $fieldArray['perms_userid'] = $this->userid;
7630
            $fieldArray['perms_groupid'] = (int)$this->BE_USER->firstMainGroup;
7631
            $fieldArray['perms_user'] = $this->assemblePermissions($this->defaultPermissions['user']);
7632
            $fieldArray['perms_group'] = $this->assemblePermissions($this->defaultPermissions['group']);
7633
            $fieldArray['perms_everybody'] = $this->assemblePermissions($this->defaultPermissions['everybody']);
7634
        }
7635
        return $fieldArray;
7636
    }
7637
7638
    /**
7639
     * 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.
7640
     *
7641
     * @param string $table Table name
7642
     * @param array $incomingFieldArray Incoming array (passed by reference)
7643
     */
7644
    public function addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
7645
    {
7646
        // Checking languages:
7647
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
7648
            if (!isset($incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
7649
                // Language field must be found in input row - otherwise it does not make sense.
7650
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
7651
                    ->getQueryBuilderForTable('sys_language');
7652
                $queryBuilder->getRestrictions()
7653
                    ->removeAll()
7654
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7655
                $queryBuilder
7656
                    ->select('uid')
7657
                    ->from('sys_language')
7658
                    ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
7659
                $rows = array_merge([['uid' => 0]], $queryBuilder->execute()->fetchAll(), [['uid' => -1]]);
7660
                foreach ($rows as $r) {
7661
                    if ($this->BE_USER->checkLanguageAccess($r['uid'])) {
7662
                        $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $r['uid'];
7663
                        break;
7664
                    }
7665
                }
7666
            }
7667
        }
7668
    }
7669
7670
    /**
7671
     * Returns the $data array from $table overridden in the fields defined in ->overrideValues.
7672
     *
7673
     * @param string $table Table name
7674
     * @param array $data Data array with fields from table. These will be overlaid with values in $this->overrideValues[$table]
7675
     * @return array Data array, processed.
7676
     */
7677
    public function overrideFieldArray($table, $data)
7678
    {
7679
        if (is_array($this->overrideValues[$table])) {
7680
            $data = array_merge($data, $this->overrideValues[$table]);
7681
        }
7682
        return $data;
7683
    }
7684
7685
    /**
7686
     * Compares the incoming field array with the current record and unsets all fields which are the same.
7687
     * Used for existing records being updated
7688
     *
7689
     * @param string $table Record table name
7690
     * @param int $id Record uid
7691
     * @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!
7692
     * @return array Returns $fieldArray. If the returned array is empty, then the record should not be updated!
7693
     */
7694
    public function compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
7695
    {
7696
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7697
        $queryBuilder = $connection->createQueryBuilder();
7698
        $queryBuilder->getRestrictions()->removeAll();
7699
        $currentRecord = $queryBuilder->select('*')
7700
            ->from($table)
7701
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7702
            ->execute()
7703
            ->fetch();
7704
        // If the current record exists (which it should...), begin comparison:
7705
        if (is_array($currentRecord)) {
7706
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
7707
            $columnRecordTypes = [];
7708
            foreach ($currentRecord as $columnName => $_) {
7709
                $columnRecordTypes[$columnName] = '';
7710
                $type = $tableDetails->getColumn($columnName)->getType();
7711
                if ($type instanceof IntegerType) {
7712
                    $columnRecordTypes[$columnName] = 'int';
7713
                }
7714
            }
7715
            // Unset the fields which are similar:
7716
            foreach ($fieldArray as $col => $val) {
7717
                $fieldConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
7718
                $isNullField = (!empty($fieldConfiguration['eval']) && GeneralUtility::inList($fieldConfiguration['eval'], 'null'));
7719
7720
                // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
7721
                // In general this avoids to store superfluous data which also will be visualized in the editing history.
7722
                if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $columnRecordTypes[$col], $isNullField)) {
7723
                    unset($fieldArray[$col]);
7724
                } else {
7725 View Code Duplication
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
7726
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
7727
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
7728
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
7729
                    }
7730 View Code Duplication
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
7731
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
7732
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
7733
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
7734
                    }
7735
                }
7736
            }
7737
        } else {
7738
            // If the current record does not exist this is an error anyways and we just return an empty array here.
7739
            $fieldArray = [];
7740
        }
7741
        return $fieldArray;
7742
    }
7743
7744
    /**
7745
     * Determines whether submitted values and stored values are equal.
7746
     * This prevents from adding superfluous field changes which would be shown in the record history as well.
7747
     * For NULL fields (see accordant TCA definition 'eval' = 'null'), a special handling is required since
7748
     * (!strcmp(NULL, '')) would be a false-positive.
7749
     *
7750
     * @param mixed $submittedValue Value that has submitted (e.g. from a backend form)
7751
     * @param mixed $storedValue Value that is currently stored in the database
7752
     * @param string $storedType SQL type of the stored value column (see mysql_field_type(), e.g 'int', 'string',  ...)
7753
     * @param bool $allowNull Whether NULL values are allowed by accordant TCA definition ('eval' = 'null')
7754
     * @return bool Whether both values are considered to be equal
7755
     */
7756
    protected function isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
7757
    {
7758
        // No NULL values are allowed, this is the regular behaviour.
7759
        // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
7760
        if (!$allowNull) {
7761
            $result = (string)$submittedValue === (string)$storedValue || $storedType === 'int' && (int)$storedValue === (int)$submittedValue;
7762
            // Null values are allowed, but currently there's a real (not NULL) value.
7763
        // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
7764
        } elseif ($storedValue !== null) {
7765
            $result = (
7766
                $submittedValue !== null
7767
                && $this->isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
7768
            );
7769
            // Null values are allowed, and currently there's a NULL value.
7770
        // Thus, check whether a NULL value was submitted.
7771
        } else {
7772
            $result = ($submittedValue === null);
7773
        }
7774
7775
        return $result;
7776
    }
7777
7778
    /**
7779
     * Calculates the bitvalue of the permissions given in a string, comma-separated
7780
     *
7781
     * @param string $string List of pMap strings
7782
     * @return int Integer mask
7783
     * @see setTSconfigPermissions(), newFieldArray()
7784
     */
7785
    public function assemblePermissions($string)
7786
    {
7787
        $keyArr = GeneralUtility::trimExplode(',', $string, true);
7788
        $value = 0;
7789
        foreach ($keyArr as $key) {
7790
            if ($key && isset($this->pMap[$key])) {
7791
                $value |= $this->pMap[$key];
7792
            }
7793
        }
7794
        return $value;
7795
    }
7796
7797
    /**
7798
     * Converts a HTML entity (like &#123;) to the character '123'
7799
     *
7800
     * @param string $input Input string
7801
     * @return string Output string
7802
     */
7803
    public function convNumEntityToByteValue($input)
7804
    {
7805
        $token = md5(microtime());
7806
        $parts = explode($token, preg_replace('/(&#([0-9]+);)/', $token . '\\2' . $token, $input));
7807
        foreach ($parts as $k => $v) {
7808
            if ($k % 2) {
7809
                $v = (int)$v;
7810
                // Just to make sure that control bytes are not converted.
7811
                if ($v > 32) {
7812
                    $parts[$k] = chr((int)$v);
7813
                }
7814
            }
7815
        }
7816
        return implode('', $parts);
7817
    }
7818
7819
    /**
7820
     * Disables the delete clause for fetching records.
7821
     * In general only undeleted records will be used. If the delete
7822
     * clause is disabled, also deleted records are taken into account.
7823
     */
7824
    public function disableDeleteClause()
7825
    {
7826
        $this->disableDeleteClause = true;
7827
    }
7828
7829
    /**
7830
     * Returns delete-clause for the $table
7831
     *
7832
     * @param string $table Table name
7833
     * @return string Delete clause
7834
     */
7835
    public function deleteClause($table)
7836
    {
7837
        // Returns the proper delete-clause if any for a table from TCA
7838 View Code Duplication
        if (!$this->disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
7839
            return ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
7840
        }
7841
        return '';
7842
    }
7843
7844
    /**
7845
     * Add delete restriction if not disabled
7846
     *
7847
     * @param QueryRestrictionContainerInterface $restrictions
7848
     */
7849
    protected function addDeleteRestriction(QueryRestrictionContainerInterface $restrictions)
7850
    {
7851
        if (!$this->disableDeleteClause) {
7852
            $restrictions->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7853
        }
7854
    }
7855
7856
    /**
7857
     * Gets UID of parent record. If record is deleted it will be looked up in
7858
     * an array built before the record was deleted
7859
     *
7860
     * @param string $table Table where record lives/lived
7861
     * @param int $uid Record UID
7862
     * @return int[] Parent UIDs
7863
     */
7864
    protected function getOriginalParentOfRecord($table, $uid)
7865
    {
7866
        if (isset(self::$recordPidsForDeletedRecords[$table][$uid])) {
7867
            return self::$recordPidsForDeletedRecords[$table][$uid];
7868
        }
7869
        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

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

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

8760
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', FlashMessage::ERROR, /** @scrutinizer ignore-type */ 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

8760
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, /** @scrutinizer ignore-type */ $msg, '', FlashMessage::ERROR, 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

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

8895
            /** @scrutinizer ignore-type */ $tableName,
Loading history...
8896
            $liveIds,
8897
            $sortingStatement
8898
        );
8899
8900
        $resolver->setWorkspaceId($this->BE_USER->workspace);
8901
        $resolver->setKeepDeletePlaceholder(false);
8902
        $resolver->setKeepMovePlaceholder(false);
8903
        $resolver->setKeepLiveIds(true);
8904
        $recordIds = $resolver->get();
8905
8906
        $records = [];
8907
        foreach ($recordIds as $recordId) {
8908
            $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

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