Test Failed
Branch master (7b1793)
by Tymoteusz
36:50 queued 18:38
created

RelationHandler::getValueArray()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 7
nc 3
nop 1
dl 0
loc 16
rs 8.8571
c 0
b 0
f 0
1
<?php
2
namespace TYPO3\CMS\Core\Database;
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 TYPO3\CMS\Backend\Utility\BackendUtility;
18
use TYPO3\CMS\Core\Database\Query\QueryHelper;
19
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
20
use TYPO3\CMS\Core\DataHandling\PlainDataResolver;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Core\Utility\MathUtility;
23
use TYPO3\CMS\Core\Versioning\VersionState;
24
25
/**
26
 * Load database groups (relations)
27
 * Used to process the relations created by the TCA element types "group" and "select" for database records.
28
 * Manages MM-relations as well.
29
 */
30
class RelationHandler
31
{
32
    /**
33
     * $fetchAllFields if false getFromDB() fetches only uid, pid, thumbnail and label fields (as defined in TCA)
34
     *
35
     * @var bool
36
     */
37
    protected $fetchAllFields = false;
38
39
    /**
40
     * If set, values that are not ids in tables are normally discarded. By this options they will be preserved.
41
     *
42
     * @var bool
43
     */
44
    public $registerNonTableValues = false;
45
46
    /**
47
     * Contains the table names as keys. The values are the id-values for each table.
48
     * Should ONLY contain proper table names.
49
     *
50
     * @var array
51
     */
52
    public $tableArray = [];
53
54
    /**
55
     * Contains items in an numeric array (table/id for each). Tablenames here might be "_NO_TABLE"
56
     *
57
     * @var array
58
     */
59
    public $itemArray = [];
60
61
    /**
62
     * Array for NON-table elements
63
     *
64
     * @var array
65
     */
66
    public $nonTableArray = [];
67
68
    /**
69
     * @var array
70
     */
71
    public $additionalWhere = [];
72
73
    /**
74
     * Deleted-column is added to additionalWhere... if this is set...
75
     *
76
     * @var bool
77
     */
78
    public $checkIfDeleted = true;
79
80
    /**
81
     * @var array
82
     */
83
    public $dbPaths = [];
84
85
    /**
86
     * Will contain the first table name in the $tablelist (for positive ids)
87
     *
88
     * @var string
89
     */
90
    public $firstTable = '';
91
92
    /**
93
     * Will contain the second table name in the $tablelist (for negative ids)
94
     *
95
     * @var string
96
     */
97
    public $secondTable = '';
98
99
    /**
100
     * If TRUE, uid_local and uid_foreign are switched, and the current table
101
     * is inserted as tablename - this means you display a foreign relation "from the opposite side"
102
     *
103
     * @var bool
104
     */
105
    public $MM_is_foreign = false;
106
107
    /**
108
     * Field name at the "local" side of the MM relation
109
     *
110
     * @var string
111
     */
112
    public $MM_oppositeField = '';
113
114
    /**
115
     * Only set if MM_is_foreign is set
116
     *
117
     * @var string
118
     */
119
    public $MM_oppositeTable = '';
120
121
    /**
122
     * Only set if MM_is_foreign is set
123
     *
124
     * @var string
125
     */
126
    public $MM_oppositeFieldConf = '';
127
128
    /**
129
     * Is empty by default; if MM_is_foreign is set and there is more than one table
130
     * allowed (on the "local" side), then it contains the first table (as a fallback)
131
     * @var string
132
     */
133
    public $MM_isMultiTableRelationship = '';
134
135
    /**
136
     * Current table => Only needed for reverse relations
137
     *
138
     * @var string
139
     */
140
    public $currentTable;
141
142
    /**
143
     * If a record should be undeleted
144
     * (so do not use the $useDeleteClause on \TYPO3\CMS\Backend\Utility\BackendUtility)
145
     *
146
     * @var bool
147
     */
148
    public $undeleteRecord;
149
150
    /**
151
     * Array of fields value pairs that should match while SELECT
152
     * and will be written into MM table if $MM_insert_fields is not set
153
     *
154
     * @var array
155
     */
156
    public $MM_match_fields = [];
157
158
    /**
159
     * This is set to TRUE if the MM table has a UID field.
160
     *
161
     * @var bool
162
     */
163
    public $MM_hasUidField;
164
165
    /**
166
     * Array of fields and value pairs used for insert in MM table
167
     *
168
     * @var array
169
     */
170
    public $MM_insert_fields = [];
171
172
    /**
173
     * Extra MM table where
174
     *
175
     * @var string
176
     */
177
    public $MM_table_where = '';
178
179
    /**
180
     * Usage of a MM field on the opposite relation.
181
     *
182
     * @var array
183
     */
184
    protected $MM_oppositeUsage;
185
186
    /**
187
     * @var bool
188
     */
189
    protected $updateReferenceIndex = true;
190
191
    /**
192
     * @var bool
193
     */
194
    protected $useLiveParentIds = true;
195
196
    /**
197
     * @var bool
198
     */
199
    protected $useLiveReferenceIds = true;
200
201
    /**
202
     * @var int
203
     */
204
    protected $workspaceId;
205
206
    /**
207
     * @var bool
208
     */
209
    protected $purged = false;
210
211
    /**
212
     * This array will be filled by getFromDB().
213
     *
214
     * @var array
215
     */
216
    public $results = [];
217
218
    /**
219
     * Gets the current workspace id.
220
     *
221
     * @return int
222
     */
223
    public function getWorkspaceId()
224
    {
225
        if (!isset($this->workspaceId)) {
226
            $this->workspaceId = (int)$GLOBALS['BE_USER']->workspace;
227
        }
228
        return $this->workspaceId;
229
    }
230
231
    /**
232
     * Sets the current workspace id.
233
     *
234
     * @param int $workspaceId
235
     */
236
    public function setWorkspaceId($workspaceId)
237
    {
238
        $this->workspaceId = (int)$workspaceId;
239
    }
240
241
    /**
242
     * Whether item array has been purged in this instance.
243
     *
244
     * @return bool
245
     */
246
    public function isPurged()
247
    {
248
        return $this->purged;
249
    }
250
251
    /**
252
     * Initialization of the class.
253
     *
254
     * @param string $itemlist List of group/select items
255
     * @param string $tablelist Comma list of tables, first table takes priority if no table is set for an entry in the list.
256
     * @param string $MMtable Name of a MM table.
257
     * @param int $MMuid Local UID for MM lookup
258
     * @param string $currentTable Current table name
259
     * @param array $conf TCA configuration for current field
260
     */
261
    public function start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = [])
262
    {
263
        $conf = (array)$conf;
264
        // SECTION: MM reverse relations
265
        $this->MM_is_foreign = (bool)$conf['MM_opposite_field'];
266
        $this->MM_oppositeField = $conf['MM_opposite_field'];
267
        $this->MM_table_where = $conf['MM_table_where'];
268
        $this->MM_hasUidField = $conf['MM_hasUidField'];
269
        $this->MM_match_fields = is_array($conf['MM_match_fields']) ? $conf['MM_match_fields'] : [];
270
        $this->MM_insert_fields = is_array($conf['MM_insert_fields']) ? $conf['MM_insert_fields'] : $this->MM_match_fields;
271
        $this->currentTable = $currentTable;
272 View Code Duplication
        if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) {
273
            $this->MM_oppositeUsage = $conf['MM_oppositeUsage'];
274
        }
275
        if ($this->MM_is_foreign) {
276
            $tmp = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
277
            // Normally, $conf['allowed'] can contain a list of tables,
278
            // but as we are looking at a MM relation from the foreign side,
279
            // it only makes sense to allow one one table in $conf['allowed']
280
            $tmp = GeneralUtility::trimExplode(',', $tmp);
281
            $this->MM_oppositeTable = $tmp[0];
282
            unset($tmp);
283
            // Only add the current table name if there is more than one allowed field
284
            // We must be sure this has been done at least once before accessing the "columns" part of TCA for a table.
285
            $this->MM_oppositeFieldConf = $GLOBALS['TCA'][$this->MM_oppositeTable]['columns'][$this->MM_oppositeField]['config'];
286
            if ($this->MM_oppositeFieldConf['allowed']) {
287
                $oppositeFieldConf_allowed = explode(',', $this->MM_oppositeFieldConf['allowed']);
288
                if (count($oppositeFieldConf_allowed) > 1 || $this->MM_oppositeFieldConf['allowed'] === '*') {
289
                    $this->MM_isMultiTableRelationship = $oppositeFieldConf_allowed[0];
290
                }
291
            }
292
        }
293
        // SECTION:	normal MM relations
294
        // If the table list is "*" then all tables are used in the list:
295
        if (trim($tablelist) === '*') {
296
            $tablelist = implode(',', array_keys($GLOBALS['TCA']));
297
        }
298
        // The tables are traversed and internal arrays are initialized:
299
        $tempTableArray = GeneralUtility::trimExplode(',', $tablelist, true);
300
        foreach ($tempTableArray as $val) {
301
            $tName = trim($val);
302
            $this->tableArray[$tName] = [];
303
            if ($this->checkIfDeleted && $GLOBALS['TCA'][$tName]['ctrl']['delete']) {
304
                $fieldN = $tName . '.' . $GLOBALS['TCA'][$tName]['ctrl']['delete'];
305
                $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
306
            }
307
        }
308
        if (is_array($this->tableArray)) {
309
            reset($this->tableArray);
310
        } else {
311
            // No tables
312
            return;
313
        }
314
        // Set first and second tables:
315
        // Is the first table
316
        $this->firstTable = key($this->tableArray);
317
        next($this->tableArray);
318
        // If the second table is set and the ID number is less than zero (later)
319
        // then the record is regarded to come from the second table...
320
        $this->secondTable = key($this->tableArray);
321
        // Now, populate the internal itemArray and tableArray arrays:
322
        // If MM, then call this function to do that:
323
        if ($MMtable) {
324
            if ($MMuid) {
325
                $this->readMM($MMtable, $MMuid);
326
                $this->purgeItemArray();
327
            } else {
328
                // Revert to readList() for new records in order to load possible default values from $itemlist
329
                $this->readList($itemlist, $conf);
330
                $this->purgeItemArray();
331
            }
332
        } elseif ($MMuid && $conf['foreign_field']) {
333
            // If not MM but foreign_field, the read the records by the foreign_field
334
            $this->readForeignField($MMuid, $conf);
335
        } else {
336
            // If not MM, then explode the itemlist by "," and traverse the list:
337
            $this->readList($itemlist, $conf);
338
            // Do automatic default_sortby, if any
339
            if ($conf['foreign_default_sortby']) {
340
                $this->sortList($conf['foreign_default_sortby']);
341
            }
342
        }
343
    }
344
345
    /**
346
     * Sets $fetchAllFields
347
     *
348
     * @param bool $allFields enables fetching of all fields in getFromDB()
349
     */
350
    public function setFetchAllFields($allFields)
351
    {
352
        $this->fetchAllFields = (bool)$allFields;
353
    }
354
355
    /**
356
     * Sets whether the reference index shall be updated.
357
     *
358
     * @param bool $updateReferenceIndex Whether the reference index shall be updated
359
     */
360
    public function setUpdateReferenceIndex($updateReferenceIndex)
361
    {
362
        $this->updateReferenceIndex = (bool)$updateReferenceIndex;
363
    }
364
365
    /**
366
     * @param bool $useLiveParentIds
367
     */
368
    public function setUseLiveParentIds($useLiveParentIds)
369
    {
370
        $this->useLiveParentIds = (bool)$useLiveParentIds;
371
    }
372
373
    /**
374
     * @param bool $useLiveReferenceIds
375
     */
376
    public function setUseLiveReferenceIds($useLiveReferenceIds)
377
    {
378
        $this->useLiveReferenceIds = (bool)$useLiveReferenceIds;
379
    }
380
381
    /**
382
     * Explodes the item list and stores the parts in the internal arrays itemArray and tableArray from MM records.
383
     *
384
     * @param string $itemlist Item list
385
     * @param array $configuration Parent field configuration
386
     */
387
    public function readList($itemlist, array $configuration)
388
    {
389
        if ((string)trim($itemlist) != '') {
390
            $tempItemArray = GeneralUtility::trimExplode(',', $itemlist);
391
            // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
392
            // if there were spaces in the list... I suppose this is better overall...
393
            foreach ($tempItemArray as $key => $val) {
394
                // Will be set to "1" if the entry was a real table/id:
395
                $isSet = 0;
396
                // Extract table name and id. This is un the formular [tablename]_[id]
397
                // where table name MIGHT contain "_", hence the reversion of the string!
398
                $val = strrev($val);
399
                $parts = explode('_', $val, 2);
400
                $theID = strrev($parts[0]);
401
                // Check that the id IS an integer:
402
                if (MathUtility::canBeInterpretedAsInteger($theID)) {
403
                    // Get the table name: If a part of the exploded string, use that.
404
                    // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
405
                    $theTable = trim($parts[1])
406
                        ? strrev(trim($parts[1]))
407
                        : ($this->secondTable && $theID < 0 ? $this->secondTable : $this->firstTable);
408
                    // If the ID is not blank and the table name is among the names in the inputted tableList
409
                    if (
410
                        (string)$theID != ''
411
                        // allow the default language '0' for the special languages configuration
412
                        && ($theID || ($configuration['special'] ?? null) === 'languages')
413
                        && $theTable && isset($this->tableArray[$theTable])
414
                    ) {
415
                        // Get ID as the right value:
416
                        $theID = $this->secondTable ? abs((int)$theID) : (int)$theID;
417
                        // Register ID/table name in internal arrays:
418
                        $this->itemArray[$key]['id'] = $theID;
419
                        $this->itemArray[$key]['table'] = $theTable;
420
                        $this->tableArray[$theTable][] = $theID;
421
                        // Set update-flag:
422
                        $isSet = 1;
423
                    }
424
                }
425
                // If it turns out that the value from the list was NOT a valid reference to a table-record,
426
                // then we might still set it as a NO_TABLE value:
427 View Code Duplication
                if (!$isSet && $this->registerNonTableValues) {
428
                    $this->itemArray[$key]['id'] = $tempItemArray[$key];
429
                    $this->itemArray[$key]['table'] = '_NO_TABLE';
430
                    $this->nonTableArray[] = $tempItemArray[$key];
431
                }
432
            }
433
434
            // Skip if not dealing with IRRE in a CSV list on a workspace
435
            if ($configuration['type'] !== 'inline' || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
436
                || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
437
                || $this->getWorkspaceId() === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])) {
438
                return;
439
            }
440
441
            // Fetch live record data
442
            if ($this->useLiveReferenceIds) {
443
                foreach ($this->itemArray as &$item) {
444
                    $item['id'] = $this->getLiveDefaultId($item['table'], $item['id']);
445
                }
446
            } else {
447
                // Directly overlay workspace data
448
                $this->itemArray = [];
449
                $foreignTable = $configuration['foreign_table'];
450
                $ids = $this->getResolver($foreignTable, $this->tableArray[$foreignTable])->get();
451
                foreach ($ids as $id) {
452
                    $this->itemArray[] = [
453
                        'id' => $id,
454
                        'table' => $foreignTable,
455
                    ];
456
                }
457
            }
458
        }
459
    }
460
461
    /**
462
     * Does a sorting on $this->itemArray depending on a default sortby field.
463
     * This is only used for automatic sorting of comma separated lists.
464
     * This function is only relevant for data that is stored in comma separated lists!
465
     *
466
     * @param string $sortby The default_sortby field/command (e.g. 'price DESC')
467
     */
468
    public function sortList($sortby)
469
    {
470
        // Sort directly without fetching additional data
471
        if ($sortby === 'uid') {
472
            usort(
473
                $this->itemArray,
474
                function ($a, $b) {
475
                    return $a['id'] < $b['id'] ? -1 : 1;
476
                }
477
            );
478
        } elseif (count($this->tableArray) === 1) {
479
            reset($this->tableArray);
480
            $table = key($this->tableArray);
481
            $uidList = implode(',', current($this->tableArray));
482
            if ($uidList) {
483
                $this->itemArray = [];
484
                $this->tableArray = [];
485
                $queryBuilder = $this->getConnectionForTableName($table)
486
                    ->createQueryBuilder();
487
                $queryBuilder->getRestrictions()->removeAll();
488
                $queryBuilder->select('uid')
489
                    ->from($table)
490
                    ->where(
491
                        $queryBuilder->expr()->in(
492
                            'uid',
493
                            $queryBuilder->createNamedParameter(
494
                                GeneralUtility::intExplode(',', $uidList),
495
                                Connection::PARAM_INT_ARRAY
496
                            )
497
                        )
498
                    );
499
                foreach (QueryHelper::parseOrderBy((string)$sortby) as $orderPair) {
500
                    list($fieldName, $order) = $orderPair;
501
                    $queryBuilder->addOrderBy($fieldName, $order);
502
                }
503
                $statement = $queryBuilder->execute();
504
                while ($row = $statement->fetch()) {
505
                    $this->itemArray[] = ['id' => $row['uid'], 'table' => $table];
506
                    $this->tableArray[$table][] = $row['uid'];
507
                }
508
            }
509
        }
510
    }
511
512
    /**
513
     * Reads the record tablename/id into the internal arrays itemArray and tableArray from MM records.
514
     * You can call this function after start if you supply no list to start()
515
     *
516
     * @param string $tableName MM Tablename
517
     * @param int $uid Local UID
518
     */
519
    public function readMM($tableName, $uid)
520
    {
521
        $key = 0;
522
        $theTable = null;
523
        $queryBuilder = $this->getConnectionForTableName($tableName)
524
            ->createQueryBuilder();
525
        $queryBuilder->getRestrictions()->removeAll();
526
        $queryBuilder->select('*')->from($tableName);
527
        // In case of a reverse relation
528
        if ($this->MM_is_foreign) {
529
            $uidLocal_field = 'uid_foreign';
530
            $uidForeign_field = 'uid_local';
531
            $sorting_field = 'sorting_foreign';
532
            if ($this->MM_isMultiTableRelationship) {
533
                // Be backwards compatible! When allowing more than one table after
534
                // having previously allowed only one table, this case applies.
535
                if ($this->currentTable == $this->MM_isMultiTableRelationship) {
536
                    $expression = $queryBuilder->expr()->orX(
537
                        $queryBuilder->expr()->eq(
538
                            'tablenames',
539
                            $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
540
                        ),
541
                        $queryBuilder->expr()->eq(
542
                            'tablenames',
543
                            $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
544
                        )
545
                    );
546
                } else {
547
                    $expression = $queryBuilder->expr()->eq(
548
                        'tablenames',
549
                        $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
550
                    );
551
                }
552
                $queryBuilder->andWhere($expression);
553
            }
554
            $theTable = $this->MM_oppositeTable;
555
        } else {
556
            // Default
557
            $uidLocal_field = 'uid_local';
558
            $uidForeign_field = 'uid_foreign';
559
            $sorting_field = 'sorting';
560
        }
561
        if ($this->MM_table_where) {
562
            $queryBuilder->andWhere(
563
                QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where))
564
            );
565
        }
566 View Code Duplication
        foreach ($this->MM_match_fields as $field => $value) {
567
            $queryBuilder->andWhere(
568
                $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
569
            );
570
        }
571
        $queryBuilder->andWhere(
572
            $queryBuilder->expr()->eq(
573
                $uidLocal_field,
574
                $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)
575
            )
576
        );
577
        $queryBuilder->orderBy($sorting_field);
578
        $statement = $queryBuilder->execute();
579
        while ($row = $statement->fetch()) {
580
            // Default
581
            if (!$this->MM_is_foreign) {
582
                // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
583
                $theTable = $row['tablenames'] ?: $this->firstTable;
584
            }
585
            if (($row[$uidForeign_field] || $theTable === 'pages') && $theTable && isset($this->tableArray[$theTable])) {
586
                $this->itemArray[$key]['id'] = $row[$uidForeign_field];
587
                $this->itemArray[$key]['table'] = $theTable;
588
                $this->tableArray[$theTable][] = $row[$uidForeign_field];
589 View Code Duplication
            } elseif ($this->registerNonTableValues) {
590
                $this->itemArray[$key]['id'] = $row[$uidForeign_field];
591
                $this->itemArray[$key]['table'] = '_NO_TABLE';
592
                $this->nonTableArray[] = $row[$uidForeign_field];
593
            }
594
            $key++;
595
        }
596
    }
597
598
    /**
599
     * Writes the internal itemArray to MM table:
600
     *
601
     * @param string $MM_tableName MM table name
602
     * @param int $uid Local UID
603
     * @param bool $prependTableName If set, then table names will always be written.
604
     */
605
    public function writeMM($MM_tableName, $uid, $prependTableName = false)
606
    {
607
        $connection = $this->getConnectionForTableName($MM_tableName);
608
        $expressionBuilder = $connection->createQueryBuilder()->expr();
609
610
        // In case of a reverse relation
611
        if ($this->MM_is_foreign) {
612
            $uidLocal_field = 'uid_foreign';
613
            $uidForeign_field = 'uid_local';
614
            $sorting_field = 'sorting_foreign';
615
        } else {
616
            // default
617
            $uidLocal_field = 'uid_local';
618
            $uidForeign_field = 'uid_foreign';
619
            $sorting_field = 'sorting';
620
        }
621
        // If there are tables...
622
        $tableC = count($this->tableArray);
623
        if ($tableC) {
624
            // Boolean: does the field "tablename" need to be filled?
625
            $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
626
            $c = 0;
627
            $additionalWhere_tablenames = '';
628
            if ($this->MM_is_foreign && $prep) {
629
                $additionalWhere_tablenames = $expressionBuilder->eq(
630
                    'tablenames',
631
                    $expressionBuilder->literal($this->currentTable)
632
                );
633
            }
634
            $additionalWhere = $expressionBuilder->andX();
635
            // Add WHERE clause if configured
636
            if ($this->MM_table_where) {
637
                $additionalWhere->add(
638
                    QueryHelper::stripLogicalOperatorPrefix(
639
                        str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where)
640
                    )
641
                );
642
            }
643
            // Select, update or delete only those relations that match the configured fields
644
            foreach ($this->MM_match_fields as $field => $value) {
645
                $additionalWhere->add($expressionBuilder->eq($field, $expressionBuilder->literal($value)));
646
            }
647
648
            $queryBuilder = $connection->createQueryBuilder();
649
            $queryBuilder->getRestrictions()->removeAll();
650
            $queryBuilder->select($uidForeign_field)
651
                ->from($MM_tableName)
652
                ->where($queryBuilder->expr()->eq(
653
                    $uidLocal_field,
654
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
655
                ))
656
                ->orderBy($sorting_field);
657
658
            if ($prep) {
659
                $queryBuilder->addSelect('tablenames');
660
            }
661
            if ($this->MM_hasUidField) {
662
                $queryBuilder->addSelect('uid');
663
            }
664
            if ($additionalWhere_tablenames) {
665
                $queryBuilder->andWhere($additionalWhere_tablenames);
666
            }
667
            if ($additionalWhere->count()) {
668
                $queryBuilder->andWhere($additionalWhere);
669
            }
670
671
            $result = $queryBuilder->execute();
672
            $oldMMs = [];
673
            // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
674
            // If the UID is present it will be used to update sorting and delete MM-records.
675
            // This is necessary if the "multiple" feature is used for the MM relations.
676
            // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
677
            $oldMMs_inclUid = [];
678
            while ($row = $result->fetch()) {
679
                if (!$this->MM_is_foreign && $prep) {
680
                    $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]];
681
                } else {
682
                    $oldMMs[] = $row[$uidForeign_field];
683
                }
684
                $oldMMs_inclUid[] = [$row['tablenames'], $row[$uidForeign_field], $row['uid']];
685
            }
686
            // For each item, insert it:
687
            foreach ($this->itemArray as $val) {
688
                $c++;
689
                if ($prep || $val['table'] === '_NO_TABLE') {
690
                    // Insert current table if needed
691
                    if ($this->MM_is_foreign) {
692
                        $tablename = $this->currentTable;
693
                    } else {
694
                        $tablename = $val['table'];
695
                    }
696
                } else {
697
                    $tablename = '';
698
                }
699
                if (!$this->MM_is_foreign && $prep) {
700
                    $item = [$val['table'], $val['id']];
701
                } else {
702
                    $item = $val['id'];
703
                }
704
                if (in_array($item, $oldMMs)) {
705
                    $oldMMs_index = array_search($item, $oldMMs);
706
                    // In principle, selecting on the UID is all we need to do
707
                    // if a uid field is available since that is unique!
708
                    // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
709
                    $queryBuilder = $connection->createQueryBuilder();
710
                    $queryBuilder->update($MM_tableName)
711
                        ->set($sorting_field, $c)
712
                        ->where(
713
                            $expressionBuilder->eq(
714
                                $uidLocal_field,
715
                                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
716
                            ),
717
                            $expressionBuilder->eq(
718
                                $uidForeign_field,
719
                                $queryBuilder->createNamedParameter($val['id'], \PDO::PARAM_INT)
720
                            )
721
                        );
722
723
                    if ($additionalWhere->count()) {
724
                        $queryBuilder->andWhere($additionalWhere);
725
                    }
726
                    if ($this->MM_hasUidField) {
727
                        $queryBuilder->andWhere(
728
                            $expressionBuilder->eq(
729
                                'uid',
730
                                $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMMs_index][2], \PDO::PARAM_INT)
731
                            )
732
                        );
733
                    }
734
                    if ($tablename) {
735
                        $queryBuilder->andWhere(
736
                            $expressionBuilder->eq(
737
                                'tablenames',
738
                                $queryBuilder->createNamedParameter($tablename, \PDO::PARAM_STR)
739
                            )
740
                        );
741
                    }
742
743
                    $queryBuilder->execute();
744
                    // Remove the item from the $oldMMs array so after this
745
                    // foreach loop only the ones that need to be deleted are in there.
746
                    unset($oldMMs[$oldMMs_index]);
747
                    // Remove the item from the $oldMMs_inclUid array so after this
748
                    // foreach loop only the ones that need to be deleted are in there.
749
                    unset($oldMMs_inclUid[$oldMMs_index]);
750
                } else {
751
                    $insertFields = $this->MM_insert_fields;
752
                    $insertFields[$uidLocal_field] = $uid;
753
                    $insertFields[$uidForeign_field] = $val['id'];
754
                    $insertFields[$sorting_field] = $c;
755
                    if ($tablename) {
756
                        $insertFields['tablenames'] = $tablename;
757
                        $insertFields = $this->completeOppositeUsageValues($tablename, $insertFields);
758
                    }
759
                    $connection->insert($MM_tableName, $insertFields);
760
                    if ($this->MM_is_foreign) {
761
                        $this->updateRefIndex($val['table'], $val['id']);
762
                    }
763
                }
764
            }
765
            // Delete all not-used relations:
766
            if (is_array($oldMMs) && !empty($oldMMs)) {
767
                $queryBuilder = $connection->createQueryBuilder();
768
                $removeClauses = $queryBuilder->expr()->orX();
769
                $updateRefIndex_records = [];
770
                foreach ($oldMMs as $oldMM_key => $mmItem) {
771
                    // If UID field is present, of course we need only use that for deleting.
772
                    if ($this->MM_hasUidField) {
773
                        $removeClauses->add($queryBuilder->expr()->eq(
774
                            'uid',
775
                            $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMM_key][2], \PDO::PARAM_INT)
776
                        ));
777 View Code Duplication
                    } else {
778
                        if (is_array($mmItem)) {
779
                            $removeClauses->add(
780
                                $queryBuilder->expr()->andX(
781
                                    $queryBuilder->expr()->eq(
782
                                        'tablenames',
783
                                        $queryBuilder->createNamedParameter($mmItem[0], \PDO::PARAM_STR)
784
                                    ),
785
                                    $queryBuilder->expr()->eq(
786
                                        $uidForeign_field,
787
                                        $queryBuilder->createNamedParameter($mmItem[1], \PDO::PARAM_INT)
788
                                    )
789
                                )
790
                            );
791
                        } else {
792
                            $removeClauses->add(
793
                                $queryBuilder->expr()->eq(
794
                                    $uidForeign_field,
795
                                    $queryBuilder->createNamedParameter($mmItem, \PDO::PARAM_INT)
796
                                )
797
                            );
798
                        }
799
                    }
800
                    if ($this->MM_is_foreign) {
801
                        if (is_array($mmItem)) {
802
                            $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]];
803
                        } else {
804
                            $updateRefIndex_records[] = [$this->firstTable, $mmItem];
805
                        }
806
                    }
807
                }
808
809
                $queryBuilder->delete($MM_tableName)
810
                    ->where(
811
                        $queryBuilder->expr()->eq(
812
                            $uidLocal_field,
813
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
814
                        ),
815
                        $removeClauses
816
                    );
817
818
                if ($additionalWhere_tablenames) {
819
                    $queryBuilder->andWhere($additionalWhere_tablenames);
820
                }
821
                if ($additionalWhere->count()) {
822
                    $queryBuilder->andWhere($additionalWhere);
823
                }
824
825
                $queryBuilder->execute();
826
827
                // Update ref index:
828
                foreach ($updateRefIndex_records as $pair) {
829
                    $this->updateRefIndex($pair[0], $pair[1]);
830
                }
831
            }
832
            // Update ref index; In DataHandler it is not certain that this will happen because
833
            // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
834
            // This could also have been fixed in updateDB in DataHandler, however I decided to do it here ...
835
            $this->updateRefIndex($this->currentTable, $uid);
836
        }
837
    }
838
839
    /**
840
     * Remaps MM table elements from one local uid to another
841
     * Does NOT update the reference index for you, must be called subsequently to do that!
842
     *
843
     * @param string $MM_tableName MM table name
844
     * @param int $uid Local, current UID
845
     * @param int $newUid Local, new UID
846
     * @param bool $prependTableName If set, then table names will always be written.
847
     */
848
    public function remapMM($MM_tableName, $uid, $newUid, $prependTableName = false)
849
    {
850
        // In case of a reverse relation
851
        if ($this->MM_is_foreign) {
852
            $uidLocal_field = 'uid_foreign';
853
        } else {
854
            // default
855
            $uidLocal_field = 'uid_local';
856
        }
857
        // If there are tables...
858
        $tableC = count($this->tableArray);
859
        if ($tableC) {
860
            $queryBuilder = $this->getConnectionForTableName($MM_tableName)
861
                ->createQueryBuilder();
862
            $queryBuilder->update($MM_tableName)
863
                ->set($uidLocal_field, (int)$newUid)
864
                ->where($queryBuilder->expr()->eq(
865
                    $uidLocal_field,
866
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
867
                ));
868
            // Boolean: does the field "tablename" need to be filled?
869
            $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
870
            if ($this->MM_is_foreign && $prep) {
871
                $queryBuilder->andWhere(
872
                    $queryBuilder->expr()->eq(
873
                        'tablenames',
874
                        $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
875
                    )
876
                );
877
            }
878
            // Add WHERE clause if configured
879
            if ($this->MM_table_where) {
880
                $queryBuilder->andWhere(
881
                    QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where))
882
                );
883
            }
884
            // Select, update or delete only those relations that match the configured fields
885 View Code Duplication
            foreach ($this->MM_match_fields as $field => $value) {
886
                $queryBuilder->andWhere(
887
                    $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
888
                );
889
            }
890
            $queryBuilder->execute();
891
        }
892
    }
893
894
    /**
895
     * Reads items from a foreign_table, that has a foreign_field (uid of the parent record) and
896
     * stores the parts in the internal array itemArray and tableArray.
897
     *
898
     * @param int $uid The uid of the parent record (this value is also on the foreign_table in the foreign_field)
899
     * @param array $conf TCA configuration for current field
900
     */
901
    public function readForeignField($uid, $conf)
902
    {
903
        if ($this->useLiveParentIds) {
904
            $uid = $this->getLiveDefaultId($this->currentTable, $uid);
905
        }
906
907
        $key = 0;
908
        $uid = (int)$uid;
909
        // skip further processing if $uid does not
910
        // point to a valid parent record
911
        if ($uid === 0) {
912
            return;
913
        }
914
915
        $foreign_table = $conf['foreign_table'];
916
        $foreign_table_field = $conf['foreign_table_field'];
917
        $useDeleteClause = !$this->undeleteRecord;
918
        $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
919
        $queryBuilder = $this->getConnectionForTableName($foreign_table)
920
            ->createQueryBuilder();
921
        $queryBuilder->getRestrictions()
922
            ->removeAll();
923
        // Use the deleteClause (e.g. "deleted=0") on this table
924
        if ($useDeleteClause) {
925
            $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
926
        }
927
928
        $queryBuilder->select('uid')
929
            ->from($foreign_table);
930
931
        // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
932 View Code Duplication
        if ($conf['symmetric_field']) {
933
            $queryBuilder->where(
934
                $queryBuilder->expr()->orX(
935
                    $queryBuilder->expr()->eq(
936
                        $conf['foreign_field'],
937
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
938
                    ),
939
                    $queryBuilder->expr()->eq(
940
                        $conf['symmetric_field'],
941
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
942
                    )
943
                )
944
            );
945
        } else {
946
            $queryBuilder->where($queryBuilder->expr()->eq(
947
                $conf['foreign_field'],
948
                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
949
            ));
950
        }
951
        // If it's requested to look for the parent uid AND the parent table,
952
        // add an additional SQL-WHERE clause
953
        if ($foreign_table_field && $this->currentTable) {
954
            $queryBuilder->andWhere(
955
                $queryBuilder->expr()->eq(
956
                    $foreign_table_field,
957
                    $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
958
                )
959
            );
960
        }
961
        // Add additional where clause if foreign_match_fields are defined
962
        foreach ($foreign_match_fields as $field => $value) {
963
            $queryBuilder->andWhere(
964
                $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
965
            );
966
        }
967
        // Select children from the live(!) workspace only
968
        if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
969
            $queryBuilder->andWhere(
970
                $queryBuilder->expr()->in(
971
                    $foreign_table . '.t3ver_wsid',
972
                    $queryBuilder->createNamedParameter([0, (int)$this->getWorkspaceId()], Connection::PARAM_INT_ARRAY)
973
                ),
974
                $queryBuilder->expr()->neq(
975
                    $foreign_table . '.pid',
976
                    $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
977
                )
978
            );
979
        }
980
        // Get the correct sorting field
981
        // Specific manual sortby for data handled by this field
982
        $sortby = '';
983
        if ($conf['foreign_sortby']) {
984
            if ($conf['symmetric_sortby'] && $conf['symmetric_field']) {
985
                // Sorting depends on, from which side of the relation we're looking at it
986
                // This requires bypassing automatic quoting and setting of the default sort direction
987
                // @TODO: Doctrine: generalize to standard SQL to guarantee database independency
988
                $queryBuilder->add(
989
                    'orderBy',
990
                    'CASE
991
						WHEN ' . $queryBuilder->expr()->eq($conf['foreign_field'], $uid) . '
992
						THEN ' . $queryBuilder->quoteIdentifier($conf['foreign_sortby']) . '
993
						ELSE ' . $queryBuilder->quoteIdentifier($conf['symmetric_sortby']) . '
994
					END'
995
                );
996
            } else {
997
                // Regular single-side behaviour
998
                $sortby = $conf['foreign_sortby'];
999
            }
1000
        } elseif ($conf['foreign_default_sortby']) {
1001
            // Specific default sortby for data handled by this field
1002
            $sortby = $conf['foreign_default_sortby'];
1003 View Code Duplication
        } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
1004
            // Manual sortby for all table records
1005
            $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1006
        } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby']) {
1007
            // Default sortby for all table records
1008
            $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
1009
        }
1010
1011
        if (!empty($sortby)) {
1012
            foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) {
1013
                list($fieldName, $sorting) = $orderPair;
1014
                $queryBuilder->addOrderBy($fieldName, $sorting);
1015
            }
1016
        }
1017
1018
        // Get the rows from storage
1019
        $rows = [];
1020
        $result = $queryBuilder->execute();
1021
        while ($row = $result->fetch()) {
1022
            $rows[$row['uid']] = $row;
1023
        }
1024
        if (!empty($rows)) {
1025
            // Retrieve the parsed and prepared ORDER BY configuration for the resolver
1026
            $sortby = $queryBuilder->getQueryPart('orderBy');
1027
            $ids = $this->getResolver($foreign_table, array_keys($rows), $sortby)->get();
1028
            foreach ($ids as $id) {
1029
                $this->itemArray[$key]['id'] = $id;
1030
                $this->itemArray[$key]['table'] = $foreign_table;
1031
                $this->tableArray[$foreign_table][] = $id;
1032
                $key++;
1033
            }
1034
        }
1035
    }
1036
1037
    /**
1038
     * Write the sorting values to a foreign_table, that has a foreign_field (uid of the parent record)
1039
     *
1040
     * @param array $conf TCA configuration for current field
1041
     * @param int $parentUid The uid of the parent record
1042
     * @param int $updateToUid If this is larger than zero it will be used as foreign UID instead of the given $parentUid (on Copy)
1043
     * @param bool $skipSorting Do not update the sorting columns, this could happen for imported values
1044
     */
1045
    public function writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = false)
1046
    {
1047
        if ($this->useLiveParentIds) {
1048
            $parentUid = $this->getLiveDefaultId($this->currentTable, $parentUid);
1049
            if (!empty($updateToUid)) {
1050
                $updateToUid = $this->getLiveDefaultId($this->currentTable, $updateToUid);
1051
            }
1052
        }
1053
1054
        $c = 0;
1055
        $foreign_table = $conf['foreign_table'];
1056
        $foreign_field = $conf['foreign_field'];
1057
        $symmetric_field = $conf['symmetric_field'];
1058
        $foreign_table_field = $conf['foreign_table_field'];
1059
        $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
1060
        // If there are table items and we have a proper $parentUid
1061
        if (MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
1062
            // If updateToUid is not a positive integer, set it to '0', so it will be ignored
1063
            if (!(MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
1064
                $updateToUid = 0;
1065
            }
1066
            $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($foreign_table);
1067
            $fields = 'uid,pid,' . $foreign_field;
1068
            // Consider the symmetric field if defined:
1069
            if ($symmetric_field) {
1070
                $fields .= ',' . $symmetric_field;
1071
            }
1072
            // Consider workspaces if defined and currently used:
1073
            if ($considerWorkspaces) {
1074
                $fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
1075
            }
1076
            // Update all items
1077
            foreach ($this->itemArray as $val) {
1078
                $uid = $val['id'];
1079
                $table = $val['table'];
1080
                $row = [];
1081
                // Fetch the current (not overwritten) relation record if we should handle symmetric relations
1082
                if ($symmetric_field || $considerWorkspaces) {
1083
                    $row = BackendUtility::getRecord($table, $uid, $fields, '', true);
1084
                    if (empty($row)) {
1085
                        continue;
1086
                    }
1087
                }
1088
                $isOnSymmetricSide = false;
1089
                if ($symmetric_field) {
1090
                    $isOnSymmetricSide = self::isOnSymmetricSide($parentUid, $conf, $row);
1091
                }
1092
                $updateValues = $foreign_match_fields;
1093
                // No update to the uid is requested, so this is the normal behaviour
1094
                // just update the fields and care about sorting
1095
                if (!$updateToUid) {
1096
                    // Always add the pointer to the parent uid
1097
                    if ($isOnSymmetricSide) {
1098
                        $updateValues[$symmetric_field] = $parentUid;
1099
                    } else {
1100
                        $updateValues[$foreign_field] = $parentUid;
1101
                    }
1102
                    // If it is configured in TCA also to store the parent table in the child record, just do it
1103
                    if ($foreign_table_field && $this->currentTable) {
1104
                        $updateValues[$foreign_table_field] = $this->currentTable;
1105
                    }
1106
                    // Update sorting columns if not to be skipped
1107
                    if (!$skipSorting) {
1108
                        // Get the correct sorting field
1109
                        // Specific manual sortby for data handled by this field
1110
                        $sortby = '';
1111
                        if ($conf['foreign_sortby']) {
1112
                            $sortby = $conf['foreign_sortby'];
1113 View Code Duplication
                        } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
1114
                            // manual sortby for all table records
1115
                            $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1116
                        }
1117
                        // Apply sorting on the symmetric side
1118
                        // (it depends on who created the relation, so what uid is in the symmetric_field):
1119
                        if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
1120
                            $sortby = $conf['symmetric_sortby'];
1121
                        } else {
1122
                            $tempSortBy = [];
1123
                            foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) {
1124
                                list($fieldName, $order) = $orderPair;
1125
                                if ($order !== null) {
1126
                                    $tempSortBy[] = implode(' ', $orderPair);
1127
                                } else {
1128
                                    $tempSortBy[] = $fieldName;
1129
                                }
1130
                            }
1131
                            $sortby = implode(',', $tempSortBy);
1132
                        }
1133
                        if ($sortby) {
1134
                            $updateValues[$sortby] = ++$c;
1135
                        }
1136
                    }
1137
                } else {
1138
                    if ($isOnSymmetricSide) {
1139
                        $updateValues[$symmetric_field] = $updateToUid;
1140
                    } else {
1141
                        $updateValues[$foreign_field] = $updateToUid;
1142
                    }
1143
                }
1144
                // Update accordant fields in the database:
1145
                if (!empty($updateValues)) {
1146
                    // Update tstamp if any foreign field value has changed
1147
                    if (!empty($GLOBALS['TCA'][$table]['ctrl']['tstamp'])) {
1148
                        $updateValues[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1149
                    }
1150
                    $this->getConnectionForTableName($table)
1151
                        ->update(
1152
                            $table,
1153
                            $updateValues,
1154
                            ['uid' => (int)$uid]
1155
                        );
1156
                    $this->updateRefIndex($table, $uid);
1157
                }
1158
                // Update accordant fields in the database for workspaces overlays/placeholders:
1159
                if ($considerWorkspaces) {
1160
                    // It's the specific versioned record -> update placeholder (if any)
1161
                    if (!empty($row['t3ver_oid']) && VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
1162
                        $this->getConnectionForTableName($table)
1163
                            ->update(
1164
                                $table,
1165
                                $updateValues,
1166
                                ['uid' => (int)$row['t3ver_oid']]
1167
                            );
1168
                    }
1169
                }
1170
            }
1171
        }
1172
    }
1173
1174
    /**
1175
     * After initialization you can extract an array of the elements from the object. Use this function for that.
1176
     *
1177
     * @param bool $prependTableName If set, then table names will ALWAYS be prepended (unless its a _NO_TABLE value)
1178
     * @return array A numeric array.
1179
     */
1180
    public function getValueArray($prependTableName = false)
1181
    {
1182
        // INIT:
1183
        $valueArray = [];
1184
        $tableC = count($this->tableArray);
1185
        // If there are tables in the table array:
1186
        if ($tableC) {
1187
            // If there are more than ONE table in the table array, then always prepend table names:
1188
            $prep = $tableC > 1 || $prependTableName;
1189
            // Traverse the array of items:
1190
            foreach ($this->itemArray as $val) {
1191
                $valueArray[] = ($prep && $val['table'] !== '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
1192
            }
1193
        }
1194
        // Return the array
1195
        return $valueArray;
1196
    }
1197
1198
    /**
1199
     * Reads all records from internal tableArray into the internal ->results array
1200
     * where keys are table names and for each table, records are stored with uids as their keys.
1201
     * If $this->fetchAllFields is false you can save a little memory
1202
     * since only uid,pid and a few other fields are selected.
1203
     *
1204
     * @return array
1205
     */
1206
    public function getFromDB()
1207
    {
1208
        // Traverses the tables listed:
1209
        foreach ($this->tableArray as $table => $val) {
1210
            if (is_array($val)) {
1211
                $itemList = implode(',', $val);
1212
                if ($itemList) {
1213
                    if ($this->fetchAllFields) {
1214
                        $fields = '*';
1215
                    } else {
1216
                        $fields = 'uid,pid';
1217 View Code Duplication
                        if ($GLOBALS['TCA'][$table]['ctrl']['label']) {
1218
                            // Titel
1219
                            $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label'];
1220
                        }
1221 View Code Duplication
                        if ($GLOBALS['TCA'][$table]['ctrl']['label_alt']) {
1222
                            // Alternative Title-Fields
1223
                            $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label_alt'];
1224
                        }
1225 View Code Duplication
                        if ($GLOBALS['TCA'][$table]['ctrl']['thumbnail']) {
1226
                            // Thumbnail
1227
                            $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['thumbnail'];
1228
                        }
1229
                    }
1230
                    $queryBuilder = $this->getConnectionForTableName($table)
1231
                        ->createQueryBuilder();
1232
                    $queryBuilder->getRestrictions()->removeAll();
1233
                    $queryBuilder->select(...(GeneralUtility::trimExplode(',', $fields, true)))
1234
                        ->from($table)
1235
                        ->where($queryBuilder->expr()->in(
1236
                            'uid',
1237
                            $queryBuilder->createNamedParameter(
1238
                                GeneralUtility::intExplode(',', $itemList),
1239
                                Connection::PARAM_INT_ARRAY
1240
                            )
1241
                        ));
1242
                    if ($this->additionalWhere[$table]) {
1243
                        $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($this->additionalWhere[$table]));
1244
                    }
1245
                    $statement = $queryBuilder->execute();
1246
                    while ($row = $statement->fetch()) {
1247
                        $this->results[$table][$row['uid']] = $row;
1248
                    }
1249
                }
1250
            }
1251
        }
1252
        return $this->results;
1253
    }
1254
1255
    /**
1256
     * This method is typically called after getFromDB().
1257
     * $this->results holds a list of resolved and valid relations,
1258
     * $this->itemArray hold a list of "selected" relations from the incoming selection array.
1259
     * The difference is that "itemArray" may hold a single table/uid combination multiple times,
1260
     * for instance in a type=group relation having multiple=true, while "results" hold each
1261
     * resolved relation only once.
1262
     * The methods creates a sanitized "itemArray" from resolved "results" list, normalized
1263
     * the return array to always contain both table name and uid, and keep incoming
1264
     * "itemArray" sort order and keeps "multiple" selections.
1265
     *
1266
     * @return array
1267
     */
1268
    public function getResolvedItemArray(): array
1269
    {
1270
        $itemArray = [];
1271
        foreach ($this->itemArray as $item) {
1272
            if (isset($this->results[$item['table']][$item['id']])) {
1273
                $itemArray[] = [
1274
                    'table' => $item['table'],
1275
                    'uid' => $item['id'],
1276
                ];
1277
            }
1278
        }
1279
        return $itemArray;
1280
    }
1281
1282
    /**
1283
     * Counts the items in $this->itemArray and puts this value in an array by default.
1284
     *
1285
     * @param bool $returnAsArray Whether to put the count value in an array
1286
     * @return mixed The plain count as integer or the same inside an array
1287
     */
1288
    public function countItems($returnAsArray = true)
1289
    {
1290
        $count = count($this->itemArray);
1291
        if ($returnAsArray) {
1292
            $count = [$count];
1293
        }
1294
        return $count;
1295
    }
1296
1297
    /**
1298
     * Update Reference Index (sys_refindex) for a record
1299
     * Should be called any almost any update to a record which could affect references inside the record.
1300
     * (copied from DataHandler)
1301
     *
1302
     * @param string $table Table name
1303
     * @param int $id Record UID
1304
     * @return array Information concerning modifications delivered by \TYPO3\CMS\Core\Database\ReferenceIndex::updateRefIndexTable()
1305
     */
1306
    public function updateRefIndex($table, $id)
1307
    {
1308
        $statisticsArray = [];
1309
        if ($this->updateReferenceIndex) {
1310
            /** @var $refIndexObj \TYPO3\CMS\Core\Database\ReferenceIndex */
1311
            $refIndexObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ReferenceIndex::class);
1312
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
1313
                $refIndexObj->setWorkspaceId($this->getWorkspaceId());
1314
            }
1315
            $refIndexObj->enableRuntimeCache();
1316
            $statisticsArray = $refIndexObj->updateRefIndexTable($table, $id);
1317
        }
1318
        return $statisticsArray;
1319
    }
1320
1321
    /**
1322
     * Converts elements in the local item array to use version ids instead of
1323
     * live ids, if possible. The most common use case is, to call that prior
1324
     * to processing with MM relations in a workspace context. For tha special
1325
     * case, ids on both side of the MM relation must use version ids if
1326
     * available.
1327
     *
1328
     * @return bool Whether items have been converted
1329
     */
1330
    public function convertItemArray()
1331
    {
1332
        $hasBeenConverted = false;
1333
1334
        // conversion is only required in a workspace context
1335
        // (the case that version ids are submitted in a live context are rare)
1336
        if ($this->getWorkspaceId() === 0) {
1337
            return $hasBeenConverted;
1338
        }
1339
1340
        foreach ($this->tableArray as $tableName => $ids) {
1341
            if (empty($ids) || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
1342
                continue;
1343
            }
1344
1345
            // convert live ids to version ids if available
1346
            $convertedIds = $this->getResolver($tableName, $ids)
1347
                ->setKeepDeletePlaceholder(false)
1348
                ->setKeepMovePlaceholder(false)
1349
                ->processVersionOverlays($ids);
1350
            foreach ($this->itemArray as $index => $item) {
1351
                if ($item['table'] !== $tableName) {
1352
                    continue;
1353
                }
1354
                $currentItemId = $item['id'];
1355
                if (
1356
                    !isset($convertedIds[$currentItemId])
1357
                    || $currentItemId === $convertedIds[$currentItemId]
1358
                ) {
1359
                    continue;
1360
                }
1361
                // adjust local item to use resolved version id
1362
                $this->itemArray[$index]['id'] = $convertedIds[$currentItemId];
1363
                $hasBeenConverted = true;
1364
            }
1365
            // update per-table reference for ids
1366
            if ($hasBeenConverted) {
1367
                $this->tableArray[$tableName] = array_values($convertedIds);
1368
            }
1369
        }
1370
1371
        return $hasBeenConverted;
1372
    }
1373
1374
    /**
1375
     * @param int|null $workspaceId
1376
     * @return bool Whether items have been purged
1377
     */
1378
    public function purgeItemArray($workspaceId = null)
1379
    {
1380
        if ($workspaceId === null) {
1381
            $workspaceId = $this->getWorkspaceId();
1382
        } else {
1383
            $workspaceId = (int)$workspaceId;
1384
        }
1385
1386
        // Ensure, only live relations are in the items Array
1387
        if ($workspaceId === 0) {
1388
            $purgeCallback = 'purgeVersionedIds';
1389
        } else {
1390
            // Otherwise, ensure that live relations are purged if version exists
1391
            $purgeCallback = 'purgeLiveVersionedIds';
1392
        }
1393
1394
        $itemArrayHasBeenPurged = $this->purgeItemArrayHandler($purgeCallback);
1395
        $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1396
        return $itemArrayHasBeenPurged;
1397
    }
1398
1399
    /**
1400
     * Removes items having a delete placeholder from $this->itemArray
1401
     *
1402
     * @return bool Whether items have been purged
1403
     */
1404
    public function processDeletePlaceholder()
1405
    {
1406
        if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1407
            return false;
1408
        }
1409
1410
        return $this->purgeItemArrayHandler('purgeDeletePlaceholder');
1411
    }
1412
1413
    /**
1414
     * Handles a purge callback on $this->itemArray
1415
     *
1416
     * @param callable $purgeCallback
1417
     * @return bool Whether items have been purged
1418
     */
1419
    protected function purgeItemArrayHandler($purgeCallback)
1420
    {
1421
        $itemArrayHasBeenPurged = false;
1422
1423
        foreach ($this->tableArray as $itemTableName => $itemIds) {
1424
            if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1425
                continue;
1426
            }
1427
1428
            $purgedItemIds = call_user_func([$this, $purgeCallback], $itemTableName, $itemIds);
1429
            $removedItemIds = array_diff($itemIds, $purgedItemIds);
1430
            foreach ($removedItemIds as $removedItemId) {
1431
                $this->removeFromItemArray($itemTableName, $removedItemId);
1432
            }
1433
            $this->tableArray[$itemTableName] = $purgedItemIds;
1434
            if (!empty($removedItemIds)) {
1435
                $itemArrayHasBeenPurged = true;
1436
            }
1437
        }
1438
1439
        return $itemArrayHasBeenPurged;
1440
    }
1441
1442
    /**
1443
     * Purges ids that are versioned.
1444
     *
1445
     * @param string $tableName
1446
     * @param array $ids
1447
     * @return array
1448
     */
1449
    protected function purgeVersionedIds($tableName, array $ids)
1450
    {
1451
        $ids = array_combine($ids, $ids);
1452
1453
        $queryBuilder = $this->getConnectionForTableName($tableName)
1454
            ->createQueryBuilder();
1455
        $queryBuilder->getRestrictions()->removeAll();
1456
        $versions = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1457
            ->from($tableName)
1458
            ->where(
1459
                $queryBuilder->expr()->eq(
1460
                    'pid',
1461
                    $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1462
                ),
1463
                $queryBuilder->expr()->in(
1464
                    't3ver_oid',
1465
                    $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
1466
                ),
1467
                $queryBuilder->expr()->neq(
1468
                    't3ver_wsid',
1469
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1470
                )
1471
            )
1472
            ->orderBy('t3ver_state', 'DESC')
1473
            ->execute()
1474
            ->fetchAll();
1475
1476 View Code Duplication
        if (!empty($versions)) {
1477
            foreach ($versions as $version) {
1478
                $versionId = $version['uid'];
1479
                if (isset($ids[$versionId])) {
1480
                    unset($ids[$versionId]);
1481
                }
1482
            }
1483
        }
1484
1485
        return array_values($ids);
1486
    }
1487
1488
    /**
1489
     * Purges ids that are live but have an accordant version.
1490
     *
1491
     * @param string $tableName
1492
     * @param array $ids
1493
     * @return array
1494
     */
1495
    protected function purgeLiveVersionedIds($tableName, array $ids)
1496
    {
1497
        $ids = array_combine($ids, $ids);
1498
1499
        $queryBuilder = $this->getConnectionForTableName($tableName)
1500
            ->createQueryBuilder();
1501
        $queryBuilder->getRestrictions()->removeAll();
1502
        $versions = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1503
            ->from($tableName)
1504
            ->where(
1505
                $queryBuilder->expr()->eq(
1506
                    'pid',
1507
                    $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1508
                ),
1509
                $queryBuilder->expr()->in(
1510
                    't3ver_oid',
1511
                    $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
1512
                ),
1513
                $queryBuilder->expr()->neq(
1514
                    't3ver_wsid',
1515
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1516
                )
1517
            )
1518
            ->orderBy('t3ver_state', 'DESC')
1519
            ->execute()
1520
            ->fetchAll();
1521
1522
        if (!empty($versions)) {
1523
            foreach ($versions as $version) {
1524
                $versionId = $version['uid'];
1525
                $liveId = $version['t3ver_oid'];
1526
                if (isset($ids[$liveId]) && isset($ids[$versionId])) {
1527
                    unset($ids[$liveId]);
1528
                }
1529
            }
1530
        }
1531
1532
        return array_values($ids);
1533
    }
1534
1535
    /**
1536
     * Purges ids that have a delete placeholder
1537
     *
1538
     * @param string $tableName
1539
     * @param array $ids
1540
     * @return array
1541
     */
1542
    protected function purgeDeletePlaceholder($tableName, array $ids)
1543
    {
1544
        $ids = array_combine($ids, $ids);
1545
1546
        $queryBuilder = $this->getConnectionForTableName($tableName)
1547
            ->createQueryBuilder();
1548
        $queryBuilder->getRestrictions()->removeAll();
1549
        $versions = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1550
            ->from($tableName)
1551
            ->where(
1552
                $queryBuilder->expr()->eq(
1553
                    'pid',
1554
                    $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1555
                ),
1556
                $queryBuilder->expr()->in(
1557
                    't3ver_oid',
1558
                    $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
1559
                ),
1560
                $queryBuilder->expr()->neq(
1561
                    't3ver_wsid',
1562
                    $queryBuilder->createNamedParameter(
1563
                        $this->getWorkspaceId(),
1564
                        \PDO::PARAM_INT
1565
                    )
1566
                ),
1567
                $queryBuilder->expr()->eq(
1568
                    't3ver_state',
1569
                    $queryBuilder->createNamedParameter(
1570
                        (string)VersionState::cast(VersionState::DELETE_PLACEHOLDER),
1571
                        \PDO::PARAM_INT
1572
                    )
1573
                )
1574
            )
1575
            ->execute()
1576
            ->fetchAll();
1577
1578 View Code Duplication
        if (!empty($versions)) {
1579
            foreach ($versions as $version) {
1580
                $liveId = $version['t3ver_oid'];
1581
                if (isset($ids[$liveId])) {
1582
                    unset($ids[$liveId]);
1583
                }
1584
            }
1585
        }
1586
1587
        return array_values($ids);
1588
    }
1589
1590
    protected function removeFromItemArray($tableName, $id)
1591
    {
1592
        foreach ($this->itemArray as $index => $item) {
1593
            if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) {
1594
                unset($this->itemArray[$index]);
1595
                return true;
1596
            }
1597
        }
1598
        return false;
1599
    }
1600
1601
    /**
1602
     * Checks, if we're looking from the "other" side, the symmetric side, to a symmetric relation.
1603
     *
1604
     * @param string $parentUid The uid of the parent record
1605
     * @param array $parentConf The TCA configuration of the parent field embedding the child records
1606
     * @param array $childRec The record row of the child record
1607
     * @return bool Returns TRUE if looking from the symmetric ("other") side to the relation.
1608
     */
1609
    public static function isOnSymmetricSide($parentUid, $parentConf, $childRec)
1610
    {
1611
        return MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1612
            && $parentConf['symmetric_field']
1613
            && $parentUid == $childRec[$parentConf['symmetric_field']];
1614
    }
1615
1616
    /**
1617
     * Completes MM values to be written by values from the opposite relation.
1618
     * This method used MM insert field or MM match fields if defined.
1619
     *
1620
     * @param string $tableName Name of the opposite table
1621
     * @param array $referenceValues Values to be written
1622
     * @return array Values to be written, possibly modified
1623
     */
1624
    protected function completeOppositeUsageValues($tableName, array $referenceValues)
1625
    {
1626
        if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1627
            return $referenceValues;
1628
        }
1629
1630
        $fieldName = $this->MM_oppositeUsage[$tableName][0];
1631
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1632
            return $referenceValues;
1633
        }
1634
1635
        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1636
        if (!empty($configuration['MM_insert_fields'])) {
1637
            $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues);
1638
        } elseif (!empty($configuration['MM_match_fields'])) {
1639
            $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1640
        }
1641
1642
        return $referenceValues;
1643
    }
1644
1645
    /**
1646
     * Gets the record uid of the live default record. If already
1647
     * pointing to the live record, the submitted record uid is returned.
1648
     *
1649
     * @param string $tableName
1650
     * @param int $id
1651
     * @return int
1652
     */
1653 View Code Duplication
    protected function getLiveDefaultId($tableName, $id)
1654
    {
1655
        $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1656
        if ($liveDefaultId === null) {
1657
            $liveDefaultId = $id;
1658
        }
1659
        return (int)$liveDefaultId;
1660
    }
1661
1662
    /**
1663
     * @param string $tableName
1664
     * @param int[] $ids
1665
     * @param array $sortingStatement
1666
     * @return PlainDataResolver
1667
     */
1668
    protected function getResolver($tableName, array $ids, array $sortingStatement = null)
1669
    {
1670
        /** @var PlainDataResolver $resolver */
1671
        $resolver = GeneralUtility::makeInstance(
1672
            PlainDataResolver::class,
1673
            $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

1673
            /** @scrutinizer ignore-type */ $tableName,
Loading history...
1674
            $ids,
1675
            $sortingStatement
1676
        );
1677
        $resolver->setWorkspaceId($this->getWorkspaceId());
1678
        $resolver->setKeepDeletePlaceholder(true);
1679
        $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1680
        return $resolver;
1681
    }
1682
1683
    /**
1684
     * @param string $tableName
1685
     * @return Connection
1686
     */
1687
    protected function getConnectionForTableName(string $tableName)
1688
    {
1689
        return GeneralUtility::makeInstance(ConnectionPool::class)
1690
            ->getConnectionForTable($tableName);
1691
    }
1692
}
1693