Passed
Push — master ( def9a8...59289d )
by
unknown
22:30 queued 09:53
created

SlugHelper   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 599
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 72
eloc 250
c 1
b 0
f 0
dl 0
loc 599
rs 2.64

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 3
A applyWorkspaceConstraint() 0 14 3
A applyRecordConstraint() 0 14 4
A applyPageIdConstraint() 0 16 2
B sanitize() 0 37 7
A resolveVersionOverlays() 0 22 3
A extract() 0 5 1
A isUniqueInTable() 0 17 1
A buildSlugForUniqueInTable() 0 3 1
A applyLanguageConstraint() 0 12 2
B resolveParentPageRecord() 0 34 7
A applySlugConstraint() 0 6 1
A buildSlug() 0 17 4
A buildSlugForUniqueInPid() 0 3 1
A createPreparedQueryBuilder() 0 24 4
F generate() 0 73 18
B isUniqueInSite() 0 57 7
A isUniqueInPid() 0 18 1
A flushRootLineCaches() 0 4 1
A buildSlugForUniqueInSite() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like SlugHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SlugHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Core\DataHandling;
19
20
use TYPO3\CMS\Backend\Utility\BackendUtility;
21
use TYPO3\CMS\Core\Cache\CacheManager;
22
use TYPO3\CMS\Core\Charset\CharsetConverter;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
25
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
26
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
27
use TYPO3\CMS\Core\DataHandling\Model\RecordState;
28
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
29
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
30
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
31
use TYPO3\CMS\Core\Site\SiteFinder;
32
use TYPO3\CMS\Core\Utility\GeneralUtility;
33
use TYPO3\CMS\Core\Utility\MathUtility;
34
use TYPO3\CMS\Core\Utility\RootlineUtility;
35
use TYPO3\CMS\Core\Utility\StringUtility;
36
use TYPO3\CMS\Core\Versioning\VersionState;
37
38
/**
39
 * Generates, sanitizes and validates slugs for a TCA field
40
 */
41
class SlugHelper
42
{
43
    /**
44
     * @var string
45
     */
46
    protected $tableName;
47
48
    /**
49
     * @var string
50
     */
51
    protected $fieldName;
52
53
    /**
54
     * @var int
55
     */
56
    protected $workspaceId;
57
58
    /**
59
     * @var array
60
     */
61
    protected $configuration = [];
62
63
    /**
64
     * @var bool
65
     */
66
    protected $workspaceEnabled;
67
68
    /**
69
     * Defines whether the slug field should start with "/".
70
     * For pages (due to rootline functionality), this is a must have, otherwise the root level page
71
     * would have an empty value.
72
     *
73
     * @var bool
74
     */
75
    protected $prependSlashInSlug;
76
77
    /**
78
     * Slug constructor.
79
     *
80
     * @param string $tableName TCA table
81
     * @param string $fieldName TCA field
82
     * @param array $configuration TCA configuration of the field
83
     * @param int $workspaceId the workspace ID to be working on.
84
     */
85
    public function __construct(string $tableName, string $fieldName, array $configuration, int $workspaceId = 0)
86
    {
87
        $this->tableName = $tableName;
88
        $this->fieldName = $fieldName;
89
        $this->configuration = $configuration;
90
        $this->workspaceId = $workspaceId;
91
92
        if ($this->tableName === 'pages' && $this->fieldName === 'slug') {
93
            $this->prependSlashInSlug = true;
94
        } else {
95
            $this->prependSlashInSlug = $this->configuration['prependSlash'] ?? false;
96
        }
97
98
        $this->workspaceEnabled = BackendUtility::isTableWorkspaceEnabled($tableName);
99
    }
100
101
    /**
102
     * Cleans a slug value so it is used directly in the path segment of a URL.
103
     *
104
     * @param string $slug
105
     * @return string
106
     */
107
    public function sanitize(string $slug): string
108
    {
109
        // Convert to lowercase + remove tags
110
        $slug = mb_strtolower($slug, 'utf-8');
111
        $slug = strip_tags($slug);
112
113
        // Convert some special tokens (space, "_" and "-") to the space character
114
        $fallbackCharacter = (string)($this->configuration['fallbackCharacter'] ?? '-');
115
        $slug = preg_replace('/[ \t\x{00A0}\-+_]+/u', $fallbackCharacter, $slug);
116
117
        if (!\Normalizer::isNormalized((string)$slug)) {
118
            $slug = \Normalizer::normalize((string)$slug);
119
        }
120
121
        // Convert extended letters to ascii equivalents
122
        // The specCharsToASCII() converts "€" to "EUR"
123
        $slug = GeneralUtility::makeInstance(CharsetConverter::class)->specCharsToASCII('utf-8', $slug);
124
125
        // Get rid of all invalid characters, but allow slashes
126
        $slug = (string)preg_replace('/[^\p{L}\p{M}0-9\/' . preg_quote($fallbackCharacter) . ']/u', '', $slug);
127
128
        // Convert multiple fallback characters to a single one
129
        if ($fallbackCharacter !== '') {
130
            $slug = (string)preg_replace('/' . preg_quote($fallbackCharacter) . '{2,}/', $fallbackCharacter, $slug);
131
        }
132
133
        // Ensure slug is lower cased after all replacement was done
134
        $slug = mb_strtolower($slug, 'utf-8');
135
        // Extract slug, thus it does not have wrapping fallback and slash characters
136
        $extractedSlug = $this->extract($slug);
137
        // Remove trailing and beginning slashes, except if the trailing slash was added, then we'll re-add it
138
        $appendTrailingSlash = $extractedSlug !== '' && substr($slug, -1) === '/';
139
        $slug = $extractedSlug . ($appendTrailingSlash ? '/' : '');
140
        if ($this->prependSlashInSlug && ($slug[0] ?? '') !== '/') {
141
            $slug = '/' . $slug;
142
        }
143
        return $slug;
144
    }
145
146
    /**
147
     * Extracts payload of slug and removes wrapping delimiters,
148
     * e.g. `/hello/world/` will become `hello/world`.
149
     *
150
     * @param string $slug
151
     * @return string
152
     */
153
    public function extract(string $slug): string
154
    {
155
        // Convert some special tokens (space, "_" and "-") to the space character
156
        $fallbackCharacter = $this->configuration['fallbackCharacter'] ?? '-';
157
        return trim($slug, $fallbackCharacter . '/');
158
    }
159
160
    /**
161
     * Used when no slug exists for a record
162
     *
163
     * @param array $recordData
164
     * @param int $pid The uid of the page to generate the slug for
165
     * @return string
166
     */
167
    public function generate(array $recordData, int $pid): string
168
    {
169
        if ($pid === 0 || (!empty($recordData['is_siteroot']) && $this->tableName === 'pages')) {
170
            return '/';
171
        }
172
        $prefix = '';
173
        if ($this->configuration['generatorOptions']['prefixParentPageSlug'] ?? false) {
174
            $languageFieldName = $GLOBALS['TCA'][$this->tableName]['ctrl']['languageField'] ?? null;
175
            $languageId = (int)($recordData[$languageFieldName] ?? 0);
176
            $parentPageRecord = $this->resolveParentPageRecord($pid, $languageId);
177
            if (is_array($parentPageRecord)) {
178
                // If the parent page has a slug, use that instead of "re-generating" the slug from the parents' page title
179
                if (!empty($parentPageRecord['slug'])) {
180
                    $rootLineItemSlug = $parentPageRecord['slug'];
181
                } else {
182
                    $rootLineItemSlug = $this->generate($parentPageRecord, (int)$parentPageRecord['pid']);
183
                }
184
                $rootLineItemSlug = trim($rootLineItemSlug, '/');
185
                if (!empty($rootLineItemSlug)) {
186
                    $prefix = $rootLineItemSlug;
187
                }
188
            }
189
        }
190
191
        $fieldSeparator = $this->configuration['generatorOptions']['fieldSeparator'] ?? '/';
192
        $slugParts = [];
193
194
        $replaceConfiguration = $this->configuration['generatorOptions']['replacements'] ?? [];
195
        foreach ($this->configuration['generatorOptions']['fields'] ?? [] as $fieldNameParts) {
196
            if (is_string($fieldNameParts)) {
197
                $fieldNameParts = GeneralUtility::trimExplode(',', $fieldNameParts);
198
            }
199
            foreach ($fieldNameParts as $fieldName) {
200
                if (!empty($recordData[$fieldName])) {
201
                    $pieceOfSlug = $recordData[$fieldName];
202
                    $pieceOfSlug = str_replace(
203
                        array_keys($replaceConfiguration),
204
                        array_values($replaceConfiguration),
205
                        $pieceOfSlug
206
                    );
207
                    $slugParts[] = $pieceOfSlug;
208
                    break;
209
                }
210
            }
211
        }
212
        $slug = implode($fieldSeparator, $slugParts);
213
        $slug = $this->sanitize($slug);
214
        // No valid data found
215
        if ($slug === '' || $slug === '/') {
216
            $slug = 'default-' . GeneralUtility::shortMD5((string)json_encode($recordData));
217
        }
218
        if ($this->prependSlashInSlug && ($slug[0] ?? '') !== '/') {
219
            $slug = '/' . $slug;
220
        }
221
        if (!empty($prefix)) {
222
            $slug = $prefix . $slug;
223
        }
224
225
        // Hook for alternative ways of filling/modifying the slug data
226
        foreach ($this->configuration['generatorOptions']['postModifiers'] ?? [] as $funcName) {
227
            $hookParameters = [
228
                'slug' => $slug,
229
                'workspaceId' => $this->workspaceId,
230
                'configuration' => $this->configuration,
231
                'record' => $recordData,
232
                'pid' => $pid,
233
                'prefix' => $prefix,
234
                'tableName' => $this->tableName,
235
                'fieldName' => $this->fieldName,
236
            ];
237
            $slug = GeneralUtility::callUserFunction($funcName, $hookParameters, $this);
238
        }
239
        return $this->sanitize($slug);
0 ignored issues
show
Bug introduced by
The method sanitize() does not exist on null. ( Ignorable by Annotation )

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

239
        return $this->/** @scrutinizer ignore-call */ sanitize($slug);

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

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

Loading history...
240
    }
241
242
    /**
243
     * Checks if there are other records with the same slug that are located on the same PID.
244
     *
245
     * @param string $slug
246
     * @param RecordState $state
247
     * @return bool
248
     */
249
    public function isUniqueInPid(string $slug, RecordState $state): bool
250
    {
251
        $pageId = (int)$state->resolveNodeIdentifier();
252
        $recordId = $state->getSubject()->getIdentifier();
253
        $languageId = $state->getContext()->getLanguageId();
254
255
        $queryBuilder = $this->createPreparedQueryBuilder();
256
        $this->applySlugConstraint($queryBuilder, $slug);
257
        $this->applyPageIdConstraint($queryBuilder, $pageId);
258
        $this->applyRecordConstraint($queryBuilder, $recordId);
259
        $this->applyLanguageConstraint($queryBuilder, $languageId);
260
        $this->applyWorkspaceConstraint($queryBuilder, $state);
261
        $statement = $queryBuilder->execute();
262
263
        $records = $this->resolveVersionOverlays(
264
            $statement->fetchAll()
265
        );
266
        return count($records) === 0;
267
    }
268
269
    /**
270
     * Check if there are other records with the same slug that are located on the same site.
271
     *
272
     * @param string $slug
273
     * @param RecordState $state
274
     * @return bool
275
     * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
276
     */
277
    public function isUniqueInSite(string $slug, RecordState $state): bool
278
    {
279
        $pageId = $state->resolveNodeAggregateIdentifier();
280
        $recordId = $state->getSubject()->getIdentifier();
281
        $languageId = $state->getContext()->getLanguageId();
282
283
        if (!MathUtility::canBeInterpretedAsInteger($pageId)) {
284
            // If this is a new page, we use the parent page to resolve the site
285
            $pageId = $state->getNode()->getIdentifier();
286
        }
287
        $pageId = (int)$pageId;
288
289
        $queryBuilder = $this->createPreparedQueryBuilder();
290
        $this->applySlugConstraint($queryBuilder, $slug);
291
        $this->applyRecordConstraint($queryBuilder, $recordId);
292
        $this->applyLanguageConstraint($queryBuilder, $languageId);
293
        $this->applyWorkspaceConstraint($queryBuilder, $state);
294
        $statement = $queryBuilder->execute();
295
296
        $records = $this->resolveVersionOverlays(
297
            $statement->fetchAll()
298
        );
299
        if (count($records) === 0) {
300
            return true;
301
        }
302
303
        // The installation contains at least ONE other record with the same slug
304
        // Now find out if it is the same root page ID
305
        $this->flushRootLineCaches();
306
        $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
307
        try {
308
            $siteOfCurrentRecord = $siteFinder->getSiteByPageId($pageId);
309
        } catch (SiteNotFoundException $e) {
310
            // Not within a site, so nothing to do
311
            // TODO: Rather than silently ignoring this misconfiguration,
312
            // a warning should be thrown here, or maybe even let the
313
            // exception bubble up and catch it in places that uses this API
314
            return true;
315
        }
316
        foreach ($records as $record) {
317
            try {
318
                $recordState = RecordStateFactory::forName($this->tableName)->fromArray($record);
319
                $siteOfExistingRecord = $siteFinder->getSiteByPageId(
320
                    (int)$recordState->resolveNodeAggregateIdentifier()
321
                );
322
            } catch (SiteNotFoundException $exception) {
323
                // In case not site is found, the record is not
324
                // organized in any site
325
                continue;
326
            }
327
            if ($siteOfExistingRecord->getRootPageId() === $siteOfCurrentRecord->getRootPageId()) {
328
                return false;
329
            }
330
        }
331
332
        // Otherwise, everything is still fine
333
        return true;
334
    }
335
336
    /**
337
     * Check if there are other records with the same slug.
338
     *
339
     * @param string $slug
340
     * @param RecordState $state
341
     * @return bool
342
     * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
343
     */
344
    public function isUniqueInTable(string $slug, RecordState $state): bool
345
    {
346
        $recordId = $state->getSubject()->getIdentifier();
347
        $languageId = $state->getContext()->getLanguageId();
348
349
        $queryBuilder = $this->createPreparedQueryBuilder();
350
        $this->applySlugConstraint($queryBuilder, $slug);
351
        $this->applyRecordConstraint($queryBuilder, $recordId);
352
        $this->applyLanguageConstraint($queryBuilder, $languageId);
353
        $this->applyWorkspaceConstraint($queryBuilder, $state);
354
        $statement = $queryBuilder->execute();
355
356
        $records = $this->resolveVersionOverlays(
357
            $statement->fetchAll()
358
        );
359
360
        return count($records) === 0;
361
    }
362
363
    /**
364
     * Ensure root line caches are flushed to avoid any issue regarding moving of pages or dynamically creating
365
     * sites while managing slugs at the same request
366
     */
367
    protected function flushRootLineCaches(): void
368
    {
369
        RootlineUtility::purgeCaches();
370
        GeneralUtility::makeInstance(CacheManager::class)->getCache('rootline')->flush();
371
    }
372
373
    /**
374
     * Generate a slug with a suffix "/mytitle-1" if that is in use already.
375
     *
376
     * @param string $slug proposed slug
377
     * @param RecordState $state
378
     * @param callable $isUnique Callback to check for uniqueness
379
     * @return string
380
     * @throws SiteNotFoundException
381
     */
382
    protected function buildSlug(string $slug, RecordState $state, callable $isUnique): string
383
    {
384
        $slug = $this->sanitize($slug);
385
        $rawValue = $this->extract($slug);
386
        $newValue = $slug;
387
        $counter = 0;
388
        while (
389
            !$isUnique($newValue, $state)
390
            && ++$counter < 100
391
        ) {
392
            $newValue = $this->sanitize($rawValue . '-' . $counter);
393
        }
394
        if ($counter === 100) {
395
            $uniqueId = StringUtility::getUniqueId();
396
            $newValue = $this->sanitize($rawValue . '-' . GeneralUtility::shortMD5($uniqueId));
397
        }
398
        return $newValue;
399
    }
400
401
    /**
402
     * Generate a slug with a suffix "/mytitle-1" if that is in use already.
403
     *
404
     * @param string $slug proposed slug
405
     * @param RecordState $state
406
     * @return string
407
     * @throws SiteNotFoundException
408
     */
409
    public function buildSlugForUniqueInSite(string $slug, RecordState $state): string
410
    {
411
        return $this->buildSlug($slug, $state, [$this, 'isUniqueInSite']);
412
    }
413
414
    /**
415
     * Generate a slug with a suffix "/mytitle-1" if the suggested slug is in use already.
416
     *
417
     * @param string $slug proposed slug
418
     * @param RecordState $state
419
     * @return string
420
     */
421
    public function buildSlugForUniqueInPid(string $slug, RecordState $state): string
422
    {
423
        return $this->buildSlug($slug, $state, [$this, 'isUniqueInPid']);
424
    }
425
426
    /**
427
     * Generate a slug with a suffix "/mytitle-1" if that is in use already.
428
     *
429
     * @param string $slug proposed slug
430
     * @param RecordState $state
431
     * @return string
432
     * @throws SiteNotFoundException
433
     */
434
    public function buildSlugForUniqueInTable(string $slug, RecordState $state): string
435
    {
436
        return $this->buildSlug($slug, $state, [$this, 'isUniqueInTable']);
437
    }
438
439
    /**
440
     * @return QueryBuilder
441
     */
442
    protected function createPreparedQueryBuilder(): QueryBuilder
443
    {
444
        $fieldNames = ['uid', 'pid', $this->fieldName];
445
        if ($this->workspaceEnabled) {
446
            $fieldNames[] = 't3ver_state';
447
            $fieldNames[] = 't3ver_oid';
448
        }
449
        $languageFieldName = $GLOBALS['TCA'][$this->tableName]['ctrl']['languageField'] ?? null;
450
        if (is_string($languageFieldName)) {
451
            $fieldNames[] = $languageFieldName;
452
        }
453
        $languageParentFieldName = $GLOBALS['TCA'][$this->tableName]['ctrl']['transOrigPointerField'] ?? null;
454
        if (is_string($languageParentFieldName)) {
455
            $fieldNames[] = $languageParentFieldName;
456
        }
457
458
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tableName);
459
        $queryBuilder->getRestrictions()
460
            ->removeAll()
461
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
462
        $queryBuilder
463
            ->select(...$fieldNames)
464
            ->from($this->tableName);
465
        return $queryBuilder;
466
    }
467
468
    /**
469
     * @param QueryBuilder $queryBuilder
470
     * @param RecordState $state
471
     */
472
    protected function applyWorkspaceConstraint(QueryBuilder $queryBuilder, RecordState $state)
473
    {
474
        if (!$this->workspaceEnabled) {
475
            return;
476
        }
477
478
        $queryBuilder->getRestrictions()->add(
479
            GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->workspaceId)
480
        );
481
482
        // Exclude the online record of a versioned record
483
        if ($state->getVersionLink()) {
484
            $queryBuilder->andWhere(
485
                $queryBuilder->expr()->neq('uid', $state->getVersionLink()->getSubject()->getIdentifier())
486
            );
487
        }
488
    }
489
490
    /**
491
     * @param QueryBuilder $queryBuilder
492
     * @param int $languageId
493
     */
494
    protected function applyLanguageConstraint(QueryBuilder $queryBuilder, int $languageId)
495
    {
496
        $languageFieldName = $GLOBALS['TCA'][$this->tableName]['ctrl']['languageField'] ?? null;
497
        if (!is_string($languageFieldName)) {
498
            return;
499
        }
500
501
        // Only check records of the given language
502
        $queryBuilder->andWhere(
503
            $queryBuilder->expr()->eq(
504
                $languageFieldName,
505
                $queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
506
            )
507
        );
508
    }
509
510
    /**
511
     * @param QueryBuilder $queryBuilder
512
     * @param string $slug
513
     */
514
    protected function applySlugConstraint(QueryBuilder $queryBuilder, string $slug)
515
    {
516
        $queryBuilder->where(
517
            $queryBuilder->expr()->eq(
518
                $this->fieldName,
519
                $queryBuilder->createNamedParameter($slug)
520
            )
521
        );
522
    }
523
524
    /**
525
     * @param QueryBuilder $queryBuilder
526
     * @param int $pageId
527
     */
528
    protected function applyPageIdConstraint(QueryBuilder $queryBuilder, int $pageId)
529
    {
530
        if ($pageId < 0) {
531
            throw new \RuntimeException(
532
                sprintf(
533
                    'Page id must be positive "%d"',
534
                    $pageId
535
                ),
536
                1534962573
537
            );
538
        }
539
540
        $queryBuilder->andWhere(
541
            $queryBuilder->expr()->eq(
542
                'pid',
543
                $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
544
            )
545
        );
546
    }
547
548
    /**
549
     * @param QueryBuilder $queryBuilder
550
     * @param string|int $recordId
551
     */
552
    protected function applyRecordConstraint(QueryBuilder $queryBuilder, $recordId)
553
    {
554
        // Exclude the current record if it is an existing record
555
        if (!MathUtility::canBeInterpretedAsInteger($recordId)) {
556
            return;
557
        }
558
559
        $queryBuilder->andWhere(
560
            $queryBuilder->expr()->neq('uid', $queryBuilder->createNamedParameter($recordId, \PDO::PARAM_INT))
561
        );
562
        if ($this->workspaceId > 0 && $this->workspaceEnabled) {
563
            $liveId = BackendUtility::getLiveVersionIdOfRecord($this->tableName, (int)$recordId) ?? $recordId;
564
            $queryBuilder->andWhere(
565
                $queryBuilder->expr()->neq('uid', $queryBuilder->createNamedParameter($liveId, \PDO::PARAM_INT))
566
            );
567
        }
568
    }
569
570
    /**
571
     * @param array $records
572
     * @return array
573
     */
574
    protected function resolveVersionOverlays(array $records): array
575
    {
576
        if (!$this->workspaceEnabled) {
577
            return $records;
578
        }
579
580
        return array_filter(
581
            array_map(
582
                function (array $record) {
583
                    BackendUtility::workspaceOL(
584
                        $this->tableName,
585
                        $record,
586
                        $this->workspaceId,
587
                        true
588
                    );
589
                    if (VersionState::cast($record['t3ver_state'] ?? null)
590
                        ->equals(VersionState::DELETE_PLACEHOLDER)) {
591
                        return null;
592
                    }
593
                    return $record;
594
                },
595
                $records
596
            )
597
        );
598
    }
599
600
    /**
601
     * Fetch a parent page, but exclude spacers, recyclers and sys-folders
602
     * @param int $pid
603
     * @param int $languageId
604
     * @return array|null
605
     */
606
    protected function resolveParentPageRecord(int $pid, int $languageId): ?array
607
    {
608
        $parentPageRecord = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $parentPageRecord is dead and can be removed.
Loading history...
609
        $rootLine = BackendUtility::BEgetRootLine($pid, '', true, ['nav_title']);
610
        $excludeDokTypes = [
611
            PageRepository::DOKTYPE_SPACER,
612
            PageRepository::DOKTYPE_RECYCLER,
613
            PageRepository::DOKTYPE_SYSFOLDER
614
        ];
615
        do {
616
            $parentPageRecord = array_shift($rootLine);
617
            // exclude spacers, recyclers and folders
618
        } while (!empty($rootLine) && in_array((int)$parentPageRecord['doktype'], $excludeDokTypes, true));
619
        if ($languageId > 0) {
620
            $languageIds = [$languageId];
621
            $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
622
623
            try {
624
                $site = $siteFinder->getSiteByPageId($pid);
625
                $siteLanguage = $site->getLanguageById($languageId);
626
                $languageIds = array_merge($languageIds, $siteLanguage->getFallbackLanguageIds());
627
            } catch (SiteNotFoundException | \InvalidArgumentException $e) {
628
                // no site or requested language available - move on
629
            }
630
631
            foreach ($languageIds as $languageId) {
632
                $localizedParentPageRecord = BackendUtility::getRecordLocalization('pages', $parentPageRecord['uid'], $languageId);
633
                if (!empty($localizedParentPageRecord)) {
634
                    $parentPageRecord = reset($localizedParentPageRecord);
635
                    break;
636
                }
637
            }
638
        }
639
        return $parentPageRecord;
640
    }
641
}
642