Passed
Pull Request — master (#2755)
by
unknown
40:09 queued 34:58
created

RoutingService::cleanUpQueryParameters()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 10
ccs 0
cts 9
cp 0
rs 10
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 12
1
<?php
2
declare(strict_types=1);
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
namespace ApacheSolrForTypo3\Solr\Routing;
18
19
use ApacheSolrForTypo3\Solr\Routing\Enhancer\SolrRouteEnhancerInterface;
20
use Psr\Http\Message\ServerRequestInterface;
21
use Psr\Http\Message\UriInterface;
22
use Psr\Log\LoggerAwareInterface;
23
use Psr\Log\LoggerAwareTrait;
24
use TYPO3\CMS\Core\Context\Context;
25
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
26
use TYPO3\CMS\Core\Http\Uri;
27
use TYPO3\CMS\Core\Routing\PageSlugCandidateProvider;
28
use TYPO3\CMS\Core\Routing\SiteMatcher;
29
use TYPO3\CMS\Core\Site\Entity\NullSite;
30
use TYPO3\CMS\Core\Site\Entity\Site;
31
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
32
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
33
use TYPO3\CMS\Core\Site\SiteFinder;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
36
/**
37
 * This service class bundles method required to process and manipulate routes.
38
 *
39
 * @author Lars Tode <[email protected]>
40
 */
41
class RoutingService implements LoggerAwareInterface
42
{
43
    use LoggerAwareTrait;
44
45
    /**
46
     * Default plugin namespace
47
     */
48
    const PLUGIN_NAMESPACE = 'tx_solr';
49
50
    /**
51
     * Settings from routing configuration
52
     *
53
     * @var array
54
     */
55
    protected $settings = [];
56
57
    /**
58
     * List of filter that are placed as path arguments
59
     *
60
     * @var array
61
     */
62
    protected $pathArguments = [];
63
64
    /**
65
     * Plugin/extension namespace
66
     *
67
     * @var string
68
     */
69
    protected $pluginNamespace = 'tx_solr';
70
71
    /**
72
     * List of TYPO3 core parameters, that we should ignore
73
     *
74
     * @see \TYPO3\CMS\Frontend\Page\CacheHashCalculator::isCoreParameter
75
     * @var string[]
76
     */
77
    protected $coreParameters = ['no_cache', 'cHash', 'id', 'MP', 'type'];
78
79
    /**
80
     * RoutingService constructor.
81
     *
82
     * @param array $settings
83
     * @param string $pluginNamespace
84
     */
85 35
    public function __construct(array $settings = [], string $pluginNamespace = self::PLUGIN_NAMESPACE)
86
    {
87 35
        $this->settings = $settings;
88 35
        $this->pluginNamespace = $pluginNamespace;
89 35
        if (empty($this->pluginNamespace)) {
90
            $this->pluginNamespace = self::PLUGIN_NAMESPACE;
91
        }
92 35
    }
93
94
    /**
95
     * Creates a clone of the current service and replace the settings inside
96
     *
97
     * @param array $settings
98
     * @return RoutingService
99
     */
100
    public function withSettings(array $settings): RoutingService
101
    {
102
        $service = clone $this;
103
        $service->settings = $settings;
104
        return $service;
105
    }
106
107
    /**
108
     * Creates a clone of the current service and replace the settings inside
109
     *
110
     * @param array $pathArguments
111
     * @return RoutingService
112
     */
113
    public function withPathArguments(array $pathArguments): RoutingService
114
    {
115
        $service = clone $this;
116
        $service->pathArguments = $pathArguments;
117
        return $service;
118
    }
119
120
    /**
121
     * Load configuration from routing configuration
122
     *
123
     * @param array $routingConfiguration
124
     * @return $this
125
     */
126
    public function fromRoutingConfiguration(array $routingConfiguration): RoutingService
127
    {
128
        if (empty($routingConfiguration) ||
129
            empty($routingConfiguration['type']) ||
130
            !$this->isRouteEnhancerForSolr((string)$routingConfiguration['type'])) {
131
            return $this;
132
        }
133
134
        if (isset($routingConfiguration['solr'])) {
135
            $this->settings = $routingConfiguration['solr'];
136
        }
137
138
        if (isset($routingConfiguration['_arguments'])) {
139
            $this->pathArguments = $routingConfiguration['_arguments'];
140
        }
141
142
143
        return $this;
144
    }
145
146
    /**
147
     * Reset the routing service
148
     *
149
     * @return $this
150
     */
151 35
    public function reset(): RoutingService
152
    {
153 35
        $this->settings = [];
154 35
        $this->pathArguments = [];
155 35
        $this->pluginNamespace = self::PLUGIN_NAMESPACE;
156 35
        return $this;
157
    }
158
159
    /**
160
     * Test if the given parameter is a Core parameter
161
     *
162
     * @see \TYPO3\CMS\Frontend\Page\CacheHashCalculator::isCoreParameter
163
     * @param string $parameterName
164
     * @return bool
165
     */
166
    public function isCoreParameter(string $parameterName): bool
167
    {
168
        return in_array($parameterName, $this->coreParameters);
169
    }
170
171
    /**
172
     * This returns the plugin namespace
173
     * @see https://docs.typo3.org/p/apache-solr-for-typo3/solr/master/en-us/Configuration/Reference/TxSolrView.html#pluginnamespace
174
     *
175
     * @return string
176
     */
177
    public function getPluginNamespace(): string
178
    {
179
        return $this->pluginNamespace;
180
    }
181
182
    /**
183
     * Determine if an enhancer is in use for Solr
184
     *
185
     * @param string $enhancerName
186
     * @return bool
187
     */
188
    public function isRouteEnhancerForSolr(string $enhancerName): bool
189
    {
190
        if (empty($enhancerName)) {
191
            return false;
192
        }
193
194
        if (!isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers'][$enhancerName])) {
195
            return false;
196
        }
197
        $className = $GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers'][$enhancerName];
198
199
        if (!class_exists($className)) {
200
            return false;
201
        }
202
203
        $interfaces = class_implements($className);
204
205
        return in_array(SolrRouteEnhancerInterface::class, $interfaces);
206
    }
207
208
    /**
209
     * Masks Solr filter inside of the query parameters
210
     *
211
     * @param string $uriPath
212
     * @return string
213
     */
214
    public function finalizePathQuery(string $uriPath): string
215
    {
216
        $pathSegments = explode('/', $uriPath);
217
        $query = array_pop($pathSegments);
218
        $queryValues = explode($this->getMultiValueSeparatorForPathSegment(), $query);
219
220
        /*
221
         * In some constellations the path query contains the facet type in front.
222
         * This leads to the result, that the query values could contain the same facet value multiple times.
223
         *
224
         * In order to avoid this behaviour, the query values need to be checked and clean up.
225
         * 1. Remove possible prefix information
226
         * 2. Apply character replacements
227
         * 3. Filter duplicate values
228
         */
229
        for ($i = 0; $i < count($queryValues); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
230
            $queryValues[$i] = urldecode($queryValues[$i]);
231
            if ($this->containsFacetAndValueSeparator((string)$queryValues[$i])) {
232
                [$facetName, $facetValue] = explode(
233
                    $this->detectFacetAndValueSeparator((string)$queryValues[$i]),
234
                    (string)$queryValues[$i],
235
                    2
236
                );
237
238
                if ($this->isPathArgument((string)$facetName)) {
239
                    $queryValues[$i] = $facetValue;
240
                }
241
242
            }
243
            $queryValues[$i] = $this->encodeStringForPathSegment($queryValues[$i]);
244
        }
245
246
        $queryValues = array_unique($queryValues);
247
        sort($queryValues);
248
249
        $pathSegments[] = implode(
250
            $this->getMultiValueSeparatorForPathSegment(),
251
            $queryValues
252
        );
253
        return implode('/', $pathSegments);
254
    }
255
256
    /**
257
     * This method checks if the query parameter should be masked.
258
     *
259
     * @return bool
260
     */
261
    public function shouldMaskQueryParameter(): bool
262
    {
263
        if (!isset($this->settings['query']['mask']) ||
264
            !(bool)$this->settings['query']['mask']) {
265
            return false;
266
        }
267
268
        $targetFields = $this->getQueryParameterMap();
269
270
        return !empty($targetFields);
271
    }
272
273
    /**
274
     * Masks Solr filter inside of the query parameters
275
     *
276
     * @param array $queryParams
277
     * @return array
278
     */
279
    public function maskQueryParameters(array $queryParams): array
280
    {
281
        if (!$this->shouldMaskQueryParameter()) {
282
            return $queryParams;
283
        }
284
285
        if (!isset($queryParams[$this->getPluginNamespace()])) {
286
            $this->logger
287
                ->error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace());
288
            return $queryParams;
289
        }
290
291
        if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) {
292
            $this->logger
293
                ->info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace());
294
            return $queryParams;
295
        }
296
        $queryParameterMap = $this->getQueryParameterMap();
297
        $newQueryParams = $queryParams;
298
299
        $newFilterArray = [];
300
        foreach ($newQueryParams[$this->getPluginNamespace()]['filter'] as $queryParamName => $queryParamValue) {
301
            $defaultSeparator = $this->detectFacetAndValueSeparator((string)$queryParamValue);
302
            [$facetName, $facetValue] = explode($defaultSeparator, $queryParamValue, 2);
303
            $keep = false;
304
            if (isset($queryParameterMap[$facetName]) &&
305
                isset($newQueryParams[$queryParameterMap[$facetName]])) {
306
                $this->logger->error(
307
                    'Mask error: Facet "' . $facetName . '" as "' . $queryParameterMap[$facetName] .
308
                    '" already in query!'
309
                );
310
                $keep = true;
311
            }
312
            if (!isset($queryParameterMap[$facetName]) || $keep) {
313
                $newFilterArray[] = $queryParamValue;
314
                continue;
315
            }
316
317
            $newQueryParams[$queryParameterMap[$facetName]] = $facetValue;
318
        }
319
320
        $newQueryParams[$this->getPluginNamespace()]['filter'] = $newFilterArray;
321
322
        return $this->cleanUpQueryParameters($newQueryParams);
323
    }
324
325
    /**
326
     * Unmask incoming parameters if needed
327
     *
328
     * @param array $queryParams
329
     * @return array
330
     */
331
    public function unmaskQueryParameters(array $queryParams): array
332
    {
333
        if (!$this->shouldMaskQueryParameter()) {
334
            return $queryParams;
335
        }
336
337
        /*
338
         * The array $queryParameterMap contains the mapping of
339
         * facet name to new url name. In order to unmask we need to switch key and values.
340
         */
341
        $queryParameterMap = $this->getQueryParameterMap();
342
        $queryParameterMapSwitched = [];
343
        foreach ($queryParameterMap as $value => $key) {
344
            $queryParameterMapSwitched[$key] = $value;
345
        }
346
347
        $newQueryParams = [];
348
        foreach ($queryParams as $queryParamName => $queryParamValue) {
349
            // A merge is needed!
350
            if (!isset($queryParameterMapSwitched[$queryParamName])) {
351
                if (isset($newQueryParams[$queryParamName])) {
352
                    $newQueryParams[$queryParamName] = array_merge_recursive(
353
                        $newQueryParams[$queryParamName],
354
                        $queryParamValue
355
                    );
356
                } else {
357
                    $newQueryParams[$queryParamName] = $queryParamValue;
358
                }
359
                continue;
360
            }
361
            if (!isset($newQueryParams[$this->getPluginNamespace()])) {
362
                $newQueryParams[$this->getPluginNamespace()] = [];
363
            }
364
            if (!isset($newQueryParams[$this->getPluginNamespace()]['filter'])) {
365
                $newQueryParams[$this->getPluginNamespace()]['filter'] = [];
366
            }
367
368
            $newQueryParams[$this->getPluginNamespace()]['filter'][] =
369
                $queryParameterMapSwitched[$queryParamName] . ':' . $queryParamValue;
370
        }
371
372
        return $this->cleanUpQueryParameters($newQueryParams);
373
    }
374
375
    /**
376
     * This method check if the query parameters should be touched or not.
377
     *
378
     * There are following requirements:
379
     * - Masking is activated and the mal is valid or
380
     * - Concat is activated
381
     *
382
     * @return bool
383
     */
384
    public function shouldConcatQueryParameters(): bool
385
    {
386
        /*
387
         * The concat will activate automatically if parameters should be masked.
388
         * This solution is less complex since not every mapping parameter needs to be tested
389
         */
390
        if ($this->shouldMaskQueryParameter()) {
391
            return true;
392
        }
393
394
        return isset($this->settings['query']['concat']) && (bool)$this->settings['query']['concat'];
395
    }
396
397
    /**
398
     * Returns the query parameter map
399
     *
400
     * Note TYPO3 core query arguments removed from the configured map!
401
     *
402
     * @return array
403
     */
404
    public function getQueryParameterMap(): array
405
    {
406
        if (!isset($this->settings['query']['map']) ||
407
            !is_array($this->settings['query']['map']) ||
408
            empty($this->settings['query']['map'])) {
409
            return [];
410
        }
411
        // TODO: Test if there is more than one value!
412
        $self = $this;
413
        return array_filter(
414
            $this->settings['query']['map'],
415
            function($value) use ($self) {
416
                return !$self->isCoreParameter($value);
417
            }
418
        );
419
    }
420
421
    /**
422
     * Group all filter values together and concat e
423
     * Note: this will just handle filter values
424
     *
425
     * IN:
426
     * tx_solr => [
427
     *   filter => [
428
     *      color:red
429
     *      product:candy
430
     *      color:blue
431
     *      taste:sour
432
     *   ]
433
     * ]
434
     *
435
     * OUT:
436
     * tx_solr => [
437
     *   filter => [
438
     *      color:blue,red
439
     *      product:candy
440
     *      taste:sour
441
     *   ]
442
     * ]
443
     * @param array $queryParams
444
     * @return array
445
     */
446
    public function concatQueryParameter(array $queryParams = []): array
447
    {
448
        if (!$this->shouldConcatQueryParameters()) {
449
            return $queryParams;
450
        }
451
452
        if (!isset($queryParams[$this->getPluginNamespace()])) {
453
            $this->logger
454
                ->error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace());
455
            return $queryParams;
456
        }
457
458
        if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) {
459
            $this->logger
460
                ->info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace());
461
            return $queryParams;
462
        }
463
464
        $queryParams[$this->getPluginNamespace()]['filter'] =
465
            $this->concatFilterValues($queryParams[$this->getPluginNamespace()]['filter']);
466
467
        return $this->cleanUpQueryParameters($queryParams);
468
    }
469
470
    /**
471
     * This method expect a filter array that should be concat instead of the whole query
472
     *
473
     * @param array $filterArray
474
     * @return array
475
     */
476
    public function concatFilterValues(array $filterArray): array
477
    {
478
        if (empty($filterArray) || !$this->shouldConcatQueryParameters()) {
479
            return $filterArray;
480
        }
481
482
        $queryParameterMap = $this->getQueryParameterMap();
483
        $newFilterArray = [];
484
        $defaultSeparator = $this->detectFacetAndValueSeparator((string)$filterArray[0]);
485
        // Collect parameter names and rename parameter if required
486
        foreach ($filterArray as $set) {
487
            $separator = $this->detectFacetAndValueSeparator((string)$set);
488
            [$facetName, $facetValue] = explode($separator, $set, 2);
489
            if (isset($queryParameterMap[$facetName])) {
490
                $facetName = $queryParameterMap[$facetName];
491
            }
492
            if (!isset($newFilterArray[$facetName])) {
493
                $newFilterArray[$facetName] = [$facetValue];
494
            } else {
495
                $newFilterArray[$facetName][] = $facetValue;
496
            }
497
        }
498
499
        foreach ($newFilterArray as $facetName => $facetValues) {
500
            $newFilterArray[$facetName] = $facetName . $defaultSeparator . $this->queryParameterFacetsToString($facetValues);
501
        }
502
503
        return array_values($newFilterArray);
504
    }
505
506
    /**
507
     * Inflate given query parameters if configured
508
     * Note: this will just combine filter values
509
     *
510
     * IN:
511
     * tx_solr => [
512
     *   filter => [
513
     *      color:blue,red
514
     *      product:candy
515
     *      taste:sour
516
     *   ]
517
     * ]
518
     *
519
     * OUT:
520
     * tx_solr => [
521
     *   filter => [
522
     *      color:red
523
     *      product:candy
524
     *      color:blue
525
     *      taste:sour
526
     *   ]
527
     * ]
528
     *
529
     * @param array $queryParams
530
     * @return array
531
     */
532
    public function inflateQueryParameter(array $queryParams = []): array
533
    {
534
        if (!$this->shouldConcatQueryParameters()) {
535
            return $queryParams;
536
        }
537
538
        if (!isset($queryParams[$this->getPluginNamespace()])) {
539
            $queryParams[$this->getPluginNamespace()] = [];
540
        }
541
542
        if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) {
543
            $queryParams[$this->getPluginNamespace()]['filter'] = [];
544
        }
545
546
        $newQueryParams = [];
547
        foreach ($queryParams[$this->getPluginNamespace()]['filter'] as $set) {
548
            $separator = $this->detectFacetAndValueSeparator((string)$set);
549
            [$facetName, $facetValuesString] = explode($separator, $set, 2);
550
551
            $facetValues = explode($this->getQueryParameterValueSeparator(), $facetValuesString);
552
            foreach ($facetValues as $facetValue) {
553
                $newQueryParams[] = $facetName . $separator . $facetValue;
554
            }
555
        }
556
        $queryParams[$this->getPluginNamespace()]['filter'] = array_values($newQueryParams);
557
558
        return $this->cleanUpQueryParameters($queryParams);
559
    }
560
561
    /**
562
     * Cleanup the query parameters, to avoid empty solr arguments
563
     *
564
     * @param array $queryParams
565
     * @return array
566
     */
567
    public function cleanUpQueryParameters(array $queryParams): array
568
    {
569
        if (empty($queryParams[$this->getPluginNamespace()]['filter'])) {
570
            unset($queryParams[$this->getPluginNamespace()]['filter']);
571
        }
572
573
        if (empty($queryParams[$this->getPluginNamespace()])) {
574
            unset($queryParams[$this->getPluginNamespace()]);
575
        }
576
        return $queryParams;
577
    }
578
579
    /**
580
     * Builds a string out of multiple facet values
581
     *
582
     * @param array $facets
583
     * @return string
584
     */
585
    public function queryParameterFacetsToString(array $facets): string
586
    {
587
        sort($facets);
588
        return implode($this->getQueryParameterValueSeparator(), $facets);
589
    }
590
591
    /**
592
     * Returns the string which separates the facet from the value
593
     *
594
     * @param string $facetWithValue
595
     * @return string
596
     */
597
    public function detectFacetAndValueSeparator(string $facetWithValue): string
598
    {
599
        $separator = ':';
600
        if (mb_strpos($facetWithValue, '%3A') !== false) {
601
            $separator = '%3A';
602
        }
603
604
        return $separator;
605
    }
606
607
    /**
608
     * Check if given facet value combination contains a separator
609
     *
610
     * @param string $facetWithValue
611
     * @return bool
612
     */
613
    public function containsFacetAndValueSeparator(string $facetWithValue): bool
614
    {
615
        if (mb_strpos($facetWithValue, ':') === false && mb_strpos($facetWithValue, '%3A') === false) {
616
            return false;
617
        }
618
619
        return true;
620
    }
621
622
    /**
623
     * Returns the mapping array to replace characters within a facet value for a given type
624
     *
625
     * @param string $type
626
     * @return array
627
     */
628
    protected function getReplacementMap(string $type): array
629
    {
630
        if (is_array($this->settings['facet-' . $type]['replaceCharacters'])) {
631
            return $this->settings['facet-' . $type]['replaceCharacters'];
632
        }
633
        if (is_array($this->settings['replaceCharacters'])) {
634
            return $this->settings['replaceCharacters'];
635
        }
636
        return [];
637
    }
638
639
    /**
640
     * Apply character map for a given type and url encode it
641
     *
642
     * @param string $type
643
     * @param string $string
644
     * @return string
645
     */
646
    public function applyCharacterReplacementForType(string $type, string $string): string
647
    {
648
        $replacementMap = $this->getReplacementMap($type);
649
        if (!empty($replacementMap)) {
650
            foreach ($replacementMap as $search => $replace) {
651
                $string = str_replace($search, $replace, $string);
652
            }
653
        }
654
655
        return urlencode($string);
656
    }
657
658
    /**
659
     * Encode a string for path segment
660
     *
661
     * @param string $string
662
     * @return string
663
     */
664
    public function encodeStringForPathSegment(string $string): string
665
    {
666
        return $this->applyCharacterReplacementForType(
667
            'path',
668
            $string
669
        );
670
    }
671
672
    /**
673
     * Encode a string for path segment
674
     *
675
     * @param string $string
676
     * @return string
677
     */
678
    public function decodeStringForPathSegment(string $string): string
679
    {
680
        $replacementMap = $this->getReplacementMap('path');
681
        $string = urldecode($string);
682
683
        if (!empty($replacementMap)) {
684
            foreach ($replacementMap as $search => $replace) {
685
                $string = str_replace($replace, $search, $string);
686
            }
687
        }
688
689
        return $string;
690
    }
691
692
    /**
693
     * Encode a string for query value
694
     *
695
     * @param string $string
696
     * @return string
697
     */
698
    public function encodeStringForQueryValue(string $string): string
699
    {
700
        return $this->applyCharacterReplacementForType(
701
            'query',
702
            $string
703
        );
704
    }
705
706
    /**
707
     * Encode a string for path segment
708
     *
709
     * @param string $string
710
     * @return string
711
     */
712
    public function decodeStringForQueryValue(string $string): string
713
    {
714
        $replacementMap = $this->getReplacementMap('query');
715
        $string = urldecode($string);
716
        if (!empty($replacementMap)) {
717
            foreach ($replacementMap as $search => $replace) {
718
                $string = str_replace($replace, $search, $string);
719
            }
720
        }
721
722
        return $string;
723
    }
724
725
    /**
726
     * Cleanup facet values (strip type if needed)
727
     *
728
     * @param array $facetValues
729
     * @return array
730
     */
731
    public function cleanupFacetValues(array $facetValues): array
732
    {
733
        for ($i = 0; $i < count($facetValues); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
734
            if (!$this->containsFacetAndValueSeparator((string)$facetValues[$i])) {
735
                continue;
736
            }
737
738
            $separator = $this->detectFacetAndValueSeparator((string)$facetValues[$i]);
739
            [$type, $value] = explode($separator, $facetValues[$i]);
740
741
            if ($this->isMappingArgument($type) || $this->isPathArgument($type)) {
742
                $facetValues[$i] = $value;
743
            }
744
        }
745
        return $facetValues;
746
    }
747
748
    /**
749
     * Builds a string out of multiple facet values
750
     *
751
     * @param array $facets
752
     * @return string
753
     */
754
    public function pathFacetsToString(array $facets): string
755
    {
756
        $facets = $this->cleanupFacetValues($facets);
757
        sort($facets);
758
        for ($i = 0; $i < count($facets); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
759
            $facets[$i] = $this->encodeStringForPathSegment($facets[$i]);
760
        }
761
        return implode($this->getDefaultMultiValueSeparator(), $facets);
762
    }
763
764
    /**
765
     * Builds a string out of multiple facet values
766
     *
767
     * @param array $facets
768
     * @return string
769
     */
770
    public function facetsToString(array $facets): string
771
    {
772
        $facets = $this->cleanupFacetValues($facets);
773
        sort($facets);
774
        return implode($this->getDefaultMultiValueSeparator(), $facets);
775
    }
776
777
    /**
778
     * Builds a string out of multiple facet values
779
     *
780
     * @param string $facets
781
     * @return array
782
     */
783
    public function pathFacetStringToArray(string $facets): array
784
    {
785
        $facets = $this->decodeStringForPathSegment($facets);
786
        return explode($this->getDefaultMultiValueSeparator(), $facets);
787
    }
788
789
    /**
790
     * Builds a string out of multiple facet values
791
     *
792
     * @param string $facets
793
     * @return array
794
     */
795
    public function facetStringToArray(string $facets): array
796
    {
797
        return explode($this->getDefaultMultiValueSeparator(), $facets);
798
    }
799
800
    /**
801
     * Returns the multi value separator
802
     * @return string
803
     */
804
    public function getDefaultMultiValueSeparator(): string
805
    {
806
        return $this->settings['multiValueSeparator'] ?? ',';
807
    }
808
809
    /**
810
     * Returns the multi value separator for query parameters
811
     *
812
     * @return string
813
     */
814
    public function getQueryParameterValueSeparator(): string
815
    {
816
        if (isset($this->settings['query']['multiValueSeparator'])) {
817
            return (string)$this->settings['query']['multiValueSeparator'];
818
        }
819
820
        // Fall back
821
        return $this->getDefaultMultiValueSeparator();
822
    }
823
824
    /**
825
     * Returns the multi value separator for query parameters
826
     *
827
     * @return string
828
     */
829
    public function getMultiValueSeparatorForPathSegment(): string
830
    {
831
        if (isset($this->settings['path']['multiValueSeparator'])) {
832
            return (string)$this->settings['path']['multiValueSeparator'];
833
        }
834
835
        // Fall back
836
        return $this->getDefaultMultiValueSeparator();
837
    }
838
839
    /**
840
     * Find a enhancer configuration by a given page id
841
     *
842
     * @param int $pageUid
843
     * @return array
844
     */
845 35
    public function fetchEnhancerByPageUid(int $pageUid): array
846
    {
847 35
        $site = $this->findSiteByUid($pageUid);
848 35
        if ($site instanceof NullSite) {
849
            return [];
850
        }
851
852 35
        return $this->fetchEnhancerInSiteConfigurationByPageUid(
853 35
            $site,
854
            $pageUid
855
        );
856
    }
857
858
    /**
859
     * Returns the route enhancer configuration by given site and page uid
860
     *
861
     * @param Site $site
862
     * @param int $pageUid
863
     * @return array
864
     */
865 35
    public function fetchEnhancerInSiteConfigurationByPageUid(Site $site, int $pageUid): array
866
    {
867 35
        $configuration = $site->getConfiguration();
868 35
        if (empty($configuration['routeEnhancers']) || !is_array($configuration['routeEnhancers'])) {
869 35
            return [];
870
        }
871
        $result = [];
872
        foreach ($configuration['routeEnhancers'] as $routing => $settings) {
873
            // Not the page we are looking for
874
            if (isset($settings['limitToPages']) &&
875
                is_array($settings['limitToPages']) &&
876
                !in_array($pageUid, $settings['limitToPages'])) {
877
                continue;
878
            }
879
880
            if (empty($settings) || !isset($settings['type']) ||
881
                !$this->isRouteEnhancerForSolr((string)$settings['type'])
882
            ) {
883
                continue;
884
            }
885
            $result[] = $settings;
886
        }
887
888
        return $result;
889
    }
890
891
    /**
892
     * Add heading slash to given slug
893
     *
894
     * @param string $slug
895
     * @return string
896
     */
897
    public function cleanupHeadingSlash(string $slug): string
898
    {
899
        if (mb_substr($slug, 0, 1) !== '/') {
900
            return '/' . $slug;
901
        } else if (mb_substr($slug, 0, 2) === '//') {
902
            return mb_substr($slug, 1, mb_strlen($slug) - 1);
903
        }
904
905
        return $slug;
906
    }
907
908
    /**
909
     * Add heading slash to given slug
910
     *
911
     * @param string $slug
912
     * @return string
913
     */
914
    public function addHeadingSlash(string $slug): string
915
    {
916
        if (mb_substr($slug, 0, 1) === '/') {
917
            return $slug;
918
        }
919
920
        return '/' . $slug;
921
    }
922
923
    /**
924
     * Remove heading slash from given slug
925
     *
926
     * @param string $slug
927
     * @return string
928
     */
929
    public function removeHeadingSlash(string $slug): string
930
    {
931
        if (mb_substr($slug, 0, 1) !== '/') {
932
            return $slug;
933
        }
934
935
        return mb_substr($slug, 1, mb_strlen($slug) - 1);
936
    }
937
938
    /**
939
     * Retrieve the site by given UID
940
     *
941
     * @param int $pageUid
942
     * @return SiteInterface
943
     */
944 35
    public function findSiteByUid(int $pageUid): SiteInterface
945
    {
946
        try {
947 35
            $site = $this->getSiteFinder()
948 35
                ->getSiteByPageId($pageUid);
949 35
            return $site;
950
        } catch (SiteNotFoundException $exception) {
951
            return new NullSite();
952
        }
953
    }
954
955
    /**
956
     * @param Site $site
957
     * @return PageSlugCandidateProvider
958
     */
959
    public function getSlugCandidateProvider(Site $site): PageSlugCandidateProvider
960
    {
961
        $context = GeneralUtility::makeInstance(Context::class);
962
        return GeneralUtility::makeInstance(
963
            PageSlugCandidateProvider::class,
964
            $context,
965
            $site,
966
            null
967
        );
968
    }
969
970
    /**
971
     * Convert the base string into a URI object
972
     *
973
     * @param string $base
974
     * @return UriInterface|null
975
     */
976
    public function convertStringIntoUri(string $base): ?UriInterface
977
    {
978
        try {
979
            /* @var Uri $uri */
980
            $uri = GeneralUtility::makeInstance(
981
                Uri::class,
982
                $base
983
            );
984
985
            return $uri;
986
        } catch (\InvalidArgumentException $argumentException) {
987
            return null;
988
        }
989
    }
990
991
    /**
992
     * In order to search for a path, a possible language prefix need to remove
993
     *
994
     * @param SiteLanguage $language
995
     * @param string $path
996
     * @return string
997
     */
998
    public function stripLanguagePrefixFromPath(SiteLanguage $language, string $path): string
999
    {
1000
        if ($language->getBase()->getPath() === '/') {
1001
            return $path;
1002
        }
1003
1004
        $pathLength = mb_strlen($language->getBase()->getPath());
1005
1006
        $path = mb_substr($path, $pathLength, mb_strlen($path) - $pathLength);
1007
        if (mb_substr($path, 0, 1) !== '/') {
1008
            $path = '/' . $path;
1009
        }
1010
1011
        return $path;
1012
    }
1013
1014
    /**
1015
     * Enrich the current query Params with data from path information
1016
     *
1017
     * @param ServerRequestInterface $request
1018
     * @param array $arguments
1019
     * @param array $parameters
1020
     * @return ServerRequestInterface
1021
     */
1022
    public function addPathArgumentsToQuery(
1023
        ServerRequestInterface $request,
1024
        array $arguments,
1025
        array $parameters
1026
    ): ServerRequestInterface {
1027
        $queryParams = $request->getQueryParams();
1028
        foreach ($arguments as $fieldName => $queryPath) {
1029
            // Skip if there is no parameter
1030
            if (!isset($parameters[$fieldName])) {
1031
                continue;
1032
            }
1033
            $pathElements = explode('/', $queryPath);
1034
1035
            if (!empty($this->pluginNamespace)) {
1036
                array_unshift($pathElements, $this->pluginNamespace);
1037
            }
1038
1039
            $queryParams = $this->processUriPathArgument(
1040
                $queryParams,
1041
                $fieldName,
1042
                $parameters,
1043
                $pathElements
1044
            );
1045
        }
1046
1047
        return $request->withQueryParams($queryParams);
1048
    }
1049
1050
    /**
1051
     * Check if given argument is a mapping argument
1052
     *
1053
     * @param string $facetName
1054
     * @return bool
1055
     */
1056
    public function isMappingArgument(string $facetName): bool
1057
    {
1058
        $map = $this->getQueryParameterMap();
1059
        if (isset($map[$facetName]) && $this->shouldMaskQueryParameter()) {
1060
            return true;
1061
        }
1062
1063
        return false;
1064
    }
1065
1066
    /**
1067
     * Check if given facet type is an path argument
1068
     *
1069
     * @param string $facetName
1070
     * @return bool
1071
     */
1072
    public function isPathArgument(string $facetName): bool
1073
    {
1074
        return isset($this->pathArguments[$facetName]);
1075
    }
1076
1077
    /**
1078
     * @param string $variable
1079
     * @return string
1080
     */
1081
    public function reviewVariable(string $variable): string
1082
    {
1083
        if (!$this->containsFacetAndValueSeparator((string)$variable)) {
1084
            return $variable;
1085
        }
1086
1087
        $separator = $this->detectFacetAndValueSeparator((string)$variable);
1088
        [$type, $value] = explode($separator, $variable, 2);
1089
1090
        return $this->isMappingArgument($type) ? $value : $variable;
1091
    }
1092
1093
    /**
1094
     * Remove type prefix from filter
1095
     *
1096
     * @param array $variables
1097
     * @return array
1098
     */
1099
    public function reviseFilterVariables(array $variables): array
1100
    {
1101
        $newVariables = [];
1102
        foreach ($variables as $key => $value) {
1103
            $matches = [];
1104
            if (!preg_match('/###' . $this->getPluginNamespace() . ':filter:\d+:(.+?)###/', $key, $matches)) {
1105
                $newVariables[$key] = $value;
1106
                continue;
1107
            }
1108
            if (!$this->isMappingArgument($matches[1]) && !$this->isPathArgument($matches[1])) {
1109
                $newVariables[$key] = $value;
1110
                continue;
1111
            }
1112
            $separator = $this->detectFacetAndValueSeparator((string)$value);
1113
            $parts = explode($separator, $value);
1114
1115
            do {
1116
                if ($parts[0] === $matches[1]) {
1117
                    array_shift($parts);
1118
                }
1119
            } while ($parts[0] === $matches[1]);
1120
1121
            $newVariables[$key] = implode($separator, $parts);
1122
        }
1123
1124
        return $newVariables;
1125
    }
1126
1127
    /**
1128
     * Converts path segment information into query parameters
1129
     *
1130
     * Example:
1131
     * /products/household
1132
     *
1133
     * tx_solr:
1134
     *      filter:
1135
     *          - type:household
1136
     *
1137
     * @param array $queryParams
1138
     * @param string $fieldName
1139
     * @param array $parameters
1140
     * @param array $pathElements
1141
     * @return array
1142
     */
1143
    protected function processUriPathArgument(
1144
        array $queryParams,
1145
        string $fieldName,
1146
        array $parameters,
1147
        array $pathElements
1148
    ): array {
1149
        $queryKey = array_shift($pathElements);
1150
        $queryKey = (string)$queryKey;
1151
1152
        $tmpQueryKey = $queryKey;
1153
        if (strpos($queryKey, '-') !== false) {
1154
            [$tmpQueryKey, $filterName] = explode('-', $tmpQueryKey, 2);
1155
        }
1156
        if (!isset($queryParams[$tmpQueryKey]) || $queryParams[$tmpQueryKey] === null) {
1157
            $queryParams[$tmpQueryKey] = [];
1158
        }
1159
1160
        if (strpos($queryKey, '-') !== false) {
1161
            [$queryKey, $filterName] = explode('-', $queryKey, 2);
1162
            // explode multiple values
1163
            $values = $this->pathFacetStringToArray($parameters[$fieldName]);
1164
            sort($values);
1165
1166
            // @TODO: Support URL data bag
1167
            foreach ($values as $value) {
1168
                $value = $this->decodeStringForPathSegment($value);
1169
                $queryParams[$queryKey][] = $filterName . ':' . $value;
1170
            }
1171
        } else {
1172
            $queryParams[$queryKey] = $this->processUriPathArgument(
1173
                $queryParams[$queryKey],
1174
                $fieldName,
1175
                $parameters,
1176
                $pathElements
1177
            );
1178
        }
1179
1180
        return $queryParams;
1181
    }
1182
1183
    /**
1184
     * Return site matcher
1185
     *
1186
     * @return SiteMatcher
1187
     */
1188
    public function getSiteMatcher(): SiteMatcher
1189
    {
1190
        return GeneralUtility::makeInstance(SiteMatcher::class, $this->getSiteFinder());
1191
    }
1192
1193
    /**
1194
     * Returns the site finder
1195
     *
1196
     * @return SiteFinder|null
1197
     */
1198 35
    protected function getSiteFinder(): ?SiteFinder
1199
    {
1200 35
        return GeneralUtility::makeInstance(SiteFinder::class);
1201
    }
1202
}
1203