Passed
Push — release-11.1.x ( 32c198...7e2b82 )
by Rafael
28:26 queued 24:03
created

RoutingService   F

Complexity

Total Complexity 145

Size/Duplication

Total Lines 1106
Duplicated Lines 0 %

Test Coverage

Coverage 11.47%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 145
eloc 362
c 5
b 0
f 0
dl 0
loc 1106
ccs 67
cts 584
cp 0.1147
rs 2

46 Methods

Rating   Name   Duplication   Size   Complexity  
A getUrlFacetPathService() 0 3 1
A __construct() 0 8 2
A initUrlFacetService() 0 6 1
A getUrlFacetQueryService() 0 3 1
A withPathArguments() 0 6 1
A isCoreParameter() 0 3 1
A isRouteEnhancerForSolr() 0 18 4
A reset() 0 6 1
A getPluginNamespace() 0 3 1
A fromRoutingConfiguration() 0 18 6
A withSettings() 0 6 1
A shouldMaskQueryParameter() 0 10 3
A finalizePathQuery() 0 41 4
A concatQueryParameter() 0 30 6
B maskQueryParameters() 0 55 11
B unmaskQueryParameters() 0 42 8
B concatFilterValues() 0 28 7
A shouldConcatQueryParameters() 0 11 3
A getQueryParameterMap() 0 13 4
A findSiteByUid() 0 8 2
A queryParameterFacetsToString() 0 5 1
A stripLanguagePrefixFromPath() 0 14 3
A isMappingArgument() 0 8 3
A pathFacetsToString() 0 7 1
A addHeadingSlash() 0 7 2
A getSiteFinder() 0 3 1
A cleanupFacetValues() 0 16 5
A convertStringIntoUri() 0 12 2
A cleanUpQueryParameters() 0 10 3
A pathFacetStringToArray() 0 8 2
A fetchEnhancerByPageUid() 0 10 2
B fetchEnhancerInSiteConfigurationByPageUid() 0 24 10
A cleanupHeadingSlash() 0 9 3
A getSlugCandidateProvider() 0 8 1
B inflateQueryParameter() 0 40 7
A isPathArgument() 0 3 1
A getDefaultMultiValueSeparator() 0 3 1
B processUriPathArgument() 0 38 6
A containsFacetAndValueSeparator() 0 7 3
A detectFacetAndValueSeparator() 0 8 2
A facetsToString() 0 5 1
A getSiteMatcher() 0 3 1
A removeHeadingSlash() 0 7 2
A reviewVariable() 0 10 3
B reviseFilterVariables() 0 26 7
A addPathArgumentsToQuery() 0 26 4

How to fix   Complexity   

Complex Class

Complex classes like RoutingService 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 RoutingService, and based on these observations, apply Extract Interface, too.

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
     * @var UrlFacetService
81
     */
82
    protected $urlFacetPathService;
83
84
    /**
85
     * @var UrlFacetService
86
     */
87
    protected $urlFacetQueryService;
88
89
    /**
90
     * RoutingService constructor.
91
     *
92
     * @param array $settings
93
     * @param string $pluginNamespace
94
     */
95
    public function __construct(array $settings = [], string $pluginNamespace = self::PLUGIN_NAMESPACE)
96
    {
97
        $this->settings = $settings;
98
        $this->pluginNamespace = $pluginNamespace;
99
        if (empty($this->pluginNamespace)) {
100
            $this->pluginNamespace = self::PLUGIN_NAMESPACE;
101
        }
102
        $this->initUrlFacetService();
103
    }
104
105
    /**
106
     * Creates a clone of the current service and replace the settings inside
107
     *
108
     * @param array $settings
109
     * @return RoutingService
110
     */
111
    public function withSettings(array $settings): RoutingService
112
    {
113
        $service = clone $this;
114
        $service->settings = $settings;
115
        $service->initUrlFacetService();
116
        return $service;
117
    }
118
119
    /**
120
     * Creates a clone of the current service and replace the settings inside
121
     *
122
     * @param array $pathArguments
123
     * @return RoutingService
124
     */
125
    public function withPathArguments(array $pathArguments): RoutingService
126
    {
127
        $service = clone $this;
128
        $service->pathArguments = $pathArguments;
129
        $service->initUrlFacetService();
130
        return $service;
131
    }
132
133
    /**
134
     * Load configuration from routing configuration
135
     *
136
     * @param array $routingConfiguration
137
     * @return $this
138
     */
139
    public function fromRoutingConfiguration(array $routingConfiguration): RoutingService
140
    {
141
        if (empty($routingConfiguration) ||
142
            empty($routingConfiguration['type']) ||
143
            !$this->isRouteEnhancerForSolr((string)$routingConfiguration['type'])) {
144
            return $this;
145
        }
146
147
        if (isset($routingConfiguration['solr'])) {
148
            $this->settings = $routingConfiguration['solr'];
149
            $this->initUrlFacetService();
150
        }
151
152
        if (isset($routingConfiguration['_arguments'])) {
153
            $this->pathArguments = $routingConfiguration['_arguments'];
154
        }
155
156
        return $this;
157
    }
158
159
    /**
160
     * Reset the routing service
161
     *
162
     * @return $this
163
     */
164
    public function reset(): RoutingService
165
    {
166
        $this->settings = [];
167
        $this->pathArguments = [];
168
        $this->pluginNamespace = self::PLUGIN_NAMESPACE;
169
        return $this;
170
    }
171
172
    /**
173
     * Initialize url facet services for different types
174
     *
175
     * @return $this
176
     */
177
    protected function initUrlFacetService(): RoutingService
178
    {
179
        $this->urlFacetPathService = new UrlFacetService('path', $this->settings);
180
        $this->urlFacetQueryService = new UrlFacetService('query', $this->settings);
181
182
        return $this;
183
    }
184
185
    /**
186
     * @return UrlFacetService
187
     */
188
    public function getUrlFacetPathService(): UrlFacetService
189
    {
190
        return $this->urlFacetPathService;
191
    }
192
193
    /**
194
     * @return UrlFacetService
195
     */
196
    public function getUrlFacetQueryService(): UrlFacetService
197
    {
198
        return $this->urlFacetQueryService;
199
    }
200
201
    /**
202
     * Test if the given parameter is a Core parameter
203
     *
204
     * @see \TYPO3\CMS\Frontend\Page\CacheHashCalculator::isCoreParameter
205
     * @param string $parameterName
206
     * @return bool
207
     */
208
    public function isCoreParameter(string $parameterName): bool
209
    {
210
        return in_array($parameterName, $this->coreParameters);
211
    }
212
213
    /**
214
     * This returns the plugin namespace
215
     * @see https://docs.typo3.org/p/apache-solr-for-typo3/solr/master/en-us/Configuration/Reference/TxSolrView.html#pluginnamespace
216
     *
217
     * @return string
218
     */
219
    public function getPluginNamespace(): string
220
    {
221
        return $this->pluginNamespace;
222
    }
223
224
    /**
225
     * Determine if an enhancer is in use for Solr
226
     *
227
     * @param string $enhancerName
228
     * @return bool
229
     */
230
    public function isRouteEnhancerForSolr(string $enhancerName): bool
231
    {
232
        if (empty($enhancerName)) {
233
            return false;
234
        }
235
236
        if (!isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers'][$enhancerName])) {
237
            return false;
238
        }
239
        $className = $GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers'][$enhancerName];
240
241
        if (!class_exists($className)) {
242
            return false;
243
        }
244
245
        $interfaces = class_implements($className);
246
247
        return in_array(SolrRouteEnhancerInterface::class, $interfaces);
248
    }
249
250
    /**
251
     * Masks Solr filter inside of the query parameters
252
     *
253
     * @param string $uriPath
254
     * @return string
255
     */
256
    public function finalizePathQuery(string $uriPath): string
257
    {
258
        $pathSegments = explode('/', $uriPath);
259
        $query = array_pop($pathSegments);
260
        $queryValues = explode($this->urlFacetPathService->getMultiValueSeparator(), $query);
261
        $queryValues = array_map([$this->urlFacetPathService, 'decodeSingleValue'], $queryValues);
262
        /*
263
         * In some constellations the path query contains the facet type in front.
264
         * This leads to the result, that the query values could contain the same facet value multiple times.
265
         *
266
         * In order to avoid this behaviour, the query values need to be checked and clean up.
267
         * 1. Remove possible prefix information
268
         * 2. Apply character replacements
269
         * 3. Filter duplicate values
270
         */
271
        $queryValuesCount = count($queryValues);
272
        for ($i = 0; $i < $queryValuesCount; $i++) {
273
            $queryValues[$i] = urldecode($queryValues[$i]);
274
            if ($this->containsFacetAndValueSeparator((string)$queryValues[$i])) {
275
                [$facetName, $facetValue] = explode(
276
                    $this->detectFacetAndValueSeparator((string)$queryValues[$i]),
277
                    (string)$queryValues[$i],
278
                    2
279
                );
280
281
                if ($this->isPathArgument((string)$facetName)) {
282
                    $queryValues[$i] = $facetValue;
283
                }
284
285
            }
286
            $queryValues[$i] = $this->urlFacetPathService->applyCharacterMap($queryValues[$i]);
287
        }
288
289
        $queryValues = array_unique($queryValues);
290
        $queryValues = array_map([$this->urlFacetPathService, 'encodeSingleValue'], $queryValues);
291
        sort($queryValues);
292
        $pathSegments[] = implode(
293
            $this->urlFacetPathService->getMultiValueSeparator(),
294
            $queryValues
295
        );
296
        return implode('/', $pathSegments);
297
    }
298
299
    /**
300
     * This method checks if the query parameter should be masked.
301
     *
302
     * @return bool
303
     */
304
    public function shouldMaskQueryParameter(): bool
305
    {
306
        if (!isset($this->settings['query']['mask']) ||
307
            !(bool)$this->settings['query']['mask']) {
308
            return false;
309
        }
310
311
        $targetFields = $this->getQueryParameterMap();
312
313
        return !empty($targetFields);
314
    }
315
316
    /**
317
     * Masks Solr filter inside of the query parameters
318
     *
319
     * @param array $queryParams
320
     * @return array
321
     */
322 1
    public function maskQueryParameters(array $queryParams): array
323
    {
324 1
        if (!$this->shouldMaskQueryParameter()) {
325
            return $queryParams;
326
        }
327
328 1
        if (!isset($queryParams[$this->getPluginNamespace()])) {
329
            $this->logger
330
                ->/** @scrutinizer ignore-call */
331
                error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace());
332
            return $queryParams;
333
        }
334
335 1
        if (!isset($queryParams[$this->getPluginNamespace()]['filter']) ||
336 1
            empty($queryParams[$this->getPluginNamespace()]['filter'])) {
337
            $this->logger
338
                ->/** @scrutinizer ignore-call */
339
                info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace());
340
            return $queryParams;
341
        }
342
343 1
        if (!is_array($queryParams[$this->getPluginNamespace()]['filter'])) {
344
            $this->logger
345
                ->/** @scrutinizer ignore-call */
346
                warning('Mask info: Filter within the Query parameters is not an array');
347
            return $queryParams;
348
        }
349
350 1
        $queryParameterMap = $this->getQueryParameterMap();
351 1
        $newQueryParams = $queryParams;
352
353 1
        $newFilterArray = [];
354 1
        foreach ($newQueryParams[$this->getPluginNamespace()]['filter'] as $queryParamName => $queryParamValue) {
355 1
            $defaultSeparator = $this->detectFacetAndValueSeparator((string)$queryParamValue);
356 1
            [$facetName, $facetValue] = explode($defaultSeparator, $queryParamValue, 2);
357 1
            $keep = false;
358 1
            if (isset($queryParameterMap[$facetName]) &&
359 1
                isset($newQueryParams[$queryParameterMap[$facetName]])) {
360
                $this->logger->/** @scrutinizer ignore-call */error(
361
                    'Mask error: Facet "' . $facetName . '" as "' . $queryParameterMap[$facetName] .
362
                    '" already in query!'
363
                );
364
                $keep = true;
365
            }
366 1
            if (!isset($queryParameterMap[$facetName]) || $keep) {
367
                $newFilterArray[] = $queryParamValue;
368
                continue;
369
            }
370
371 1
            $newQueryParams[$queryParameterMap[$facetName]] = $facetValue;
372
        }
373
374 1
        $newQueryParams[$this->getPluginNamespace()]['filter'] = $newFilterArray;
375
376 1
        return $this->cleanUpQueryParameters($newQueryParams);
377
    }
378
379
    /**
380
     * Unmask incoming parameters if needed
381
     *
382
     * @param array $queryParams
383
     * @return array
384
     */
385
    public function unmaskQueryParameters(array $queryParams): array
386
    {
387
        if (!$this->shouldMaskQueryParameter()) {
388
            return $queryParams;
389
        }
390
391
        /*
392
         * The array $queryParameterMap contains the mapping of
393
         * facet name to new url name. In order to unmask we need to switch key and values.
394
         */
395
        $queryParameterMap = $this->getQueryParameterMap();
396
        $queryParameterMapSwitched = [];
397
        foreach ($queryParameterMap as $value => $key) {
398
            $queryParameterMapSwitched[$key] = $value;
399
        }
400
401
        $newQueryParams = [];
402
        foreach ($queryParams as $queryParamName => $queryParamValue) {
403
            // A merge is needed!
404
            if (!isset($queryParameterMapSwitched[$queryParamName])) {
405
                if (isset($newQueryParams[$queryParamName])) {
406
                    $newQueryParams[$queryParamName] = array_merge_recursive(
407
                        $newQueryParams[$queryParamName],
408
                        $queryParamValue
409
                    );
410
                } else {
411
                    $newQueryParams[$queryParamName] = $queryParamValue;
412
                }
413
                continue;
414
            }
415
            if (!isset($newQueryParams[$this->getPluginNamespace()])) {
416
                $newQueryParams[$this->getPluginNamespace()] = [];
417
            }
418
            if (!isset($newQueryParams[$this->getPluginNamespace()]['filter'])) {
419
                $newQueryParams[$this->getPluginNamespace()]['filter'] = [];
420
            }
421
422
            $newQueryParams[$this->getPluginNamespace()]['filter'][] =
423
                $queryParameterMapSwitched[$queryParamName] . ':' . $queryParamValue;
424
        }
425
426
        return $this->cleanUpQueryParameters($newQueryParams);
427
    }
428
429
    /**
430
     * This method check if the query parameters should be touched or not.
431
     *
432
     * There are following requirements:
433
     * - Masking is activated and the mal is valid or
434
     * - Concat is activated
435
     *
436
     * @return bool
437
     */
438
    public function shouldConcatQueryParameters(): bool
439
    {
440
        /*
441
         * The concat will activate automatically if parameters should be masked.
442
         * This solution is less complex since not every mapping parameter needs to be tested
443
         */
444
        if ($this->shouldMaskQueryParameter()) {
445
            return true;
446
        }
447
448
        return isset($this->settings['query']['concat']) && (bool)$this->settings['query']['concat'];
449
    }
450
451
    /**
452
     * Returns the query parameter map
453
     *
454
     * Note TYPO3 core query arguments removed from the configured map!
455
     *
456
     * @return array
457
     */
458
    public function getQueryParameterMap(): array
459
    {
460
        if (!isset($this->settings['query']['map']) ||
461
            !is_array($this->settings['query']['map']) ||
462
            empty($this->settings['query']['map'])) {
463
            return [];
464
        }
465
        // TODO: Test if there is more than one value!
466
        $self = $this;
467
        return array_filter(
468
            $this->settings['query']['map'],
469
            function($value) use ($self) {
470
                return !$self->isCoreParameter($value);
471
            }
472
        );
473
    }
474
475
    /**
476
     * Group all filter values together and concat e
477
     * Note: this will just handle filter values
478
     *
479
     * IN:
480
     * tx_solr => [
481
     *   filter => [
482
     *      color:red
483
     *      product:candy
484
     *      color:blue
485
     *      taste:sour
486
     *   ]
487
     * ]
488
     *
489
     * OUT:
490
     * tx_solr => [
491
     *   filter => [
492
     *      color:blue,red
493
     *      product:candy
494
     *      taste:sour
495
     *   ]
496
     * ]
497
     * @param array $queryParams
498
     * @return array
499
     */
500 1
    public function concatQueryParameter(array $queryParams = []): array
501
    {
502 1
        if (!$this->shouldConcatQueryParameters()) {
503
            return $queryParams;
504
        }
505
506 1
        if (!isset($queryParams[$this->getPluginNamespace()])) {
507
            $this->logger
508
                ->error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace());
0 ignored issues
show
Bug introduced by
The method error() 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

508
                ->/** @scrutinizer ignore-call */ 
509
                  error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace());

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...
509
            return $queryParams;
510
        }
511
512 1
        if (!isset($queryParams[$this->getPluginNamespace()]['filter']) ||
513 1
            empty($queryParams[$this->getPluginNamespace()]['filter'])) {
514
            $this->logger
515
                ->info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace());
516
            return $queryParams;
517
        }
518
519 1
        if (!is_array($queryParams[$this->getPluginNamespace()]['filter'])) {
520
            $this->logger
521
                ->/** @scrutinizer ignore-call */
522
                warning('Mask info: Filter within the Query parameters is not an array');
523
            return $queryParams;
524
        }
525
526 1
        $queryParams[$this->getPluginNamespace()]['filter'] =
527 1
            $this->concatFilterValues($queryParams[$this->getPluginNamespace()]['filter']);
528
529 1
        return $this->cleanUpQueryParameters($queryParams);
530
    }
531
532
    /**
533
     * This method expect a filter array that should be concat instead of the whole query
534
     *
535
     * @param array $filterArray
536
     * @return array
537
     */
538
    public function concatFilterValues(array $filterArray): array
539
    {
540
        if (empty($filterArray) || !$this->shouldConcatQueryParameters()) {
541
            return $filterArray;
542
        }
543
544
        $queryParameterMap = $this->getQueryParameterMap();
545
        $newFilterArray = [];
546
        $defaultSeparator = $this->detectFacetAndValueSeparator((string)$filterArray[0]);
547
        // Collect parameter names and rename parameter if required
548
        foreach ($filterArray as $set) {
549
            $separator = $this->detectFacetAndValueSeparator((string)$set);
550
            [$facetName, $facetValue] = explode($separator, $set, 2);
551
            if (isset($queryParameterMap[$facetName])) {
552
                $facetName = $queryParameterMap[$facetName];
553
            }
554
            if (!isset($newFilterArray[$facetName])) {
555
                $newFilterArray[$facetName] = [$facetValue];
556
            } else {
557
                $newFilterArray[$facetName][] = $facetValue;
558
            }
559
        }
560
561
        foreach ($newFilterArray as $facetName => $facetValues) {
562
            $newFilterArray[$facetName] = $facetName . $defaultSeparator . $this->queryParameterFacetsToString($facetValues);
563
        }
564
565
        return array_values($newFilterArray);
566
    }
567
568
    /**
569
     * Inflate given query parameters if configured
570
     * Note: this will just combine filter values
571
     *
572
     * IN:
573
     * tx_solr => [
574
     *   filter => [
575
     *      color:blue,red
576
     *      product:candy
577
     *      taste:sour
578
     *   ]
579
     * ]
580
     *
581
     * OUT:
582
     * tx_solr => [
583
     *   filter => [
584
     *      color:red
585
     *      product:candy
586
     *      color:blue
587
     *      taste:sour
588
     *   ]
589
     * ]
590
     *
591
     * @param array $queryParams
592
     * @return array
593
     */
594 1
    public function inflateQueryParameter(array $queryParams = []): array
595
    {
596 1
        if (!$this->shouldConcatQueryParameters()) {
597
            return $queryParams;
598
        }
599
600 1
        if (!isset($queryParams[$this->getPluginNamespace()])) {
601
            $queryParams[$this->getPluginNamespace()] = [];
602
        }
603
604 1
        if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) {
605
            $queryParams[$this->getPluginNamespace()]['filter'] = [];
606
        }
607
608 1
        $newQueryParams = [];
609 1
        foreach ($queryParams[$this->getPluginNamespace()]['filter'] as $set) {
610 1
            $separator = $this->detectFacetAndValueSeparator((string)$set);
611 1
            [$facetName, $facetValuesString] = explode($separator, $set, 2);
612 1
            if ($facetValuesString == null) {
613
                continue;
614
            }
615 1
            $facetValues = explode($this->urlFacetQueryService->getMultiValueSeparator(), $facetValuesString);
616
617
            /**
618
             * A facet value could contain the multi value separator. This value is masked in order to
619
             * avoid problems during separation of the values (line above).
620
             *
621
             * After splitting the values, the character inside the value need to be restored
622
             *
623
             * @see RoutingService::queryParameterFacetsToString
624
             */
625 1
            $facetValues = array_map([$this->urlFacetQueryService, 'decodeSingleValue'], $facetValues);
626
627 1
            foreach ($facetValues as $facetValue) {
628 1
                $newQueryParams[] = $facetName . $separator . $facetValue;
629
            }
630
        }
631 1
        $queryParams[$this->getPluginNamespace()]['filter'] = array_values($newQueryParams);
632
633 1
        return $this->cleanUpQueryParameters($queryParams);
634
    }
635
636
    /**
637
     * Cleanup the query parameters, to avoid empty solr arguments
638
     *
639
     * @param array $queryParams
640
     * @return array
641
     */
642
    public function cleanUpQueryParameters(array $queryParams): array
643
    {
644
        if (empty($queryParams[$this->getPluginNamespace()]['filter'])) {
645
            unset($queryParams[$this->getPluginNamespace()]['filter']);
646
        }
647
648
        if (empty($queryParams[$this->getPluginNamespace()])) {
649
            unset($queryParams[$this->getPluginNamespace()]);
650
        }
651
        return $queryParams;
652
    }
653
654
    /**
655
     * Builds a string out of multiple facet values
656
     *
657
     * A facet value could contain the multi value separator. This value have to masked in order to
658
     * avoid problems during separation of the values later.
659
     *
660
     * This mask have to apply before contact the values
661
     *
662
     * @param array $facets
663
     * @return string
664
     */
665
    public function queryParameterFacetsToString(array $facets): string
666
    {
667
        $facets = array_map([$this->urlFacetQueryService, 'encodeSingleValue'], $facets);
668
        sort($facets);
669
        return implode($this->urlFacetQueryService->getMultiValueSeparator(), $facets);
670
    }
671
672
    /**
673
     * Returns the string which separates the facet from the value
674
     *
675
     * @param string $facetWithValue
676
     * @return string
677
     */
678
    public function detectFacetAndValueSeparator(string $facetWithValue): string
679
    {
680
        $separator = ':';
681
        if (mb_strpos($facetWithValue, '%3A') !== false) {
682
            $separator = '%3A';
683
        }
684
685
        return $separator;
686
    }
687
688
    /**
689
     * Check if given facet value combination contains a separator
690
     *
691
     * @param string $facetWithValue
692
     * @return bool
693
     */
694
    public function containsFacetAndValueSeparator(string $facetWithValue): bool
695
    {
696
        if (mb_strpos($facetWithValue, ':') === false && mb_strpos($facetWithValue, '%3A') === false) {
697
            return false;
698
        }
699
700
        return true;
701
    }
702
703
    /**
704
     * Cleanup facet values (strip type if needed)
705
     *
706
     * @param array $facetValues
707
     * @return array
708
     */
709
    public function cleanupFacetValues(array $facetValues): array
710
    {
711
        $facetValuesCount = count($facetValues);
712
        for ($i = 0; $i < $facetValuesCount; $i++) {
713
            if (!$this->containsFacetAndValueSeparator((string)$facetValues[$i])) {
714
                continue;
715
            }
716
717
            $separator = $this->detectFacetAndValueSeparator((string)$facetValues[$i]);
718
            [$type, $value] = explode($separator, $facetValues[$i]);
719
720
            if ($this->isMappingArgument($type) || $this->isPathArgument($type)) {
721
                $facetValues[$i] = $value;
722
            }
723
        }
724
        return $facetValues;
725
    }
726
727
    /**
728
     * Builds a string out of multiple facet values
729
     *
730
     * @param array $facets
731
     * @return string
732
     */
733
    public function pathFacetsToString(array $facets): string
734
    {
735
        $facets = $this->cleanupFacetValues($facets);
736
        sort($facets);
737
        $facets = array_map([$this->urlFacetPathService, 'applyCharacterMap'], $facets);
738
        $facets = array_map([$this->urlFacetPathService, 'encodeSingleValue'], $facets);
739
        return implode($this->urlFacetPathService->getMultiValueSeparator(), $facets);
740
    }
741
742
    /**
743
     * Builds a string out of multiple facet values
744
     *
745
     * @param array $facets
746
     * @return string
747
     */
748 2
    public function facetsToString(array $facets): string
749
    {
750 2
        $facets = $this->cleanupFacetValues($facets);
751 2
        sort($facets);
752 2
        return implode($this->getDefaultMultiValueSeparator(), $facets);
753
    }
754
755
    /**
756
     * Builds a string out of multiple facet values
757
     *
758
     * This method is used in two different situation
759
     *  1. Middleware: Here the values should not be decoded
760
     *  2. Within the event listener CachedPathVariableModifier
761
     *
762
     * @param string $facets
763
     * @param bool $decode
764
     * @return array
765
     */
766
    public function pathFacetStringToArray(string $facets, bool $decode = true): array
767
    {
768
        $facetString = $this->urlFacetPathService->applyCharacterMap($facets);
769
        $facets = explode($this->urlFacetPathService->getMultiValueSeparator(), $facetString);
770
        if (!$decode) {
771
            return $facets;
772
        }
773
        return array_map([$this->urlFacetPathService, 'decodeSingleValue'], $facets);
774
    }
775
776
    /**
777
     * Returns the multi value separator
778
     * @return string
779
     */
780 2
    public function getDefaultMultiValueSeparator(): string
781
    {
782 2
        return $this->settings['multiValueSeparator'] ?? ',';
783
    }
784
785
    /**
786
     * Find a enhancer configuration by a given page id
787
     *
788
     * @param int $pageUid
789
     * @return array
790
     */
791
    public function fetchEnhancerByPageUid(int $pageUid): array
792
    {
793
        $site = $this->findSiteByUid($pageUid);
794
        if ($site instanceof NullSite) {
795
            return [];
796
        }
797
798
        return $this->fetchEnhancerInSiteConfigurationByPageUid(
799
            $site,
800
            $pageUid
801
        );
802
    }
803
804
    /**
805
     * Returns the route enhancer configuration by given site and page uid
806
     *
807
     * @param Site $site
808
     * @param int $pageUid
809
     * @return array
810
     */
811
    public function fetchEnhancerInSiteConfigurationByPageUid(Site $site, int $pageUid): array
812
    {
813
        $configuration = $site->getConfiguration();
814
        if (empty($configuration['routeEnhancers']) || !is_array($configuration['routeEnhancers'])) {
815
            return [];
816
        }
817
        $result = [];
818
        foreach ($configuration['routeEnhancers'] as $routing => $settings) {
819
            // Not the page we are looking for
820
            if (isset($settings['limitToPages']) &&
821
                is_array($settings['limitToPages']) &&
822
                !in_array($pageUid, $settings['limitToPages'])) {
823
                continue;
824
            }
825
826
            if (empty($settings) || !isset($settings['type']) ||
827
                !$this->isRouteEnhancerForSolr((string)$settings['type'])
828
            ) {
829
                continue;
830
            }
831
            $result[] = $settings;
832
        }
833
834
        return $result;
835
    }
836
837
    /**
838
     * Add heading slash to given slug
839
     *
840
     * @param string $slug
841
     * @return string
842
     */
843
    public function cleanupHeadingSlash(string $slug): string
844
    {
845
        if (mb_substr($slug, 0, 1) !== '/') {
846
            return '/' . $slug;
847
        } else if (mb_substr($slug, 0, 2) === '//') {
848
            return mb_substr($slug, 1, mb_strlen($slug) - 1);
849
        }
850
851
        return $slug;
852
    }
853
854
    /**
855
     * Add heading slash to given slug
856
     *
857
     * @param string $slug
858
     * @return string
859
     */
860
    public function addHeadingSlash(string $slug): string
861
    {
862
        if (mb_substr($slug, 0, 1) === '/') {
863
            return $slug;
864
        }
865
866
        return '/' . $slug;
867
    }
868
869
    /**
870
     * Remove heading slash from given slug
871
     *
872
     * @param string $slug
873
     * @return string
874
     */
875
    public function removeHeadingSlash(string $slug): string
876
    {
877
        if (mb_substr($slug, 0, 1) !== '/') {
878
            return $slug;
879
        }
880
881
        return mb_substr($slug, 1, mb_strlen($slug) - 1);
882
    }
883
884
    /**
885
     * Retrieve the site by given UID
886
     *
887
     * @param int $pageUid
888
     * @return SiteInterface
889
     */
890
    public function findSiteByUid(int $pageUid): SiteInterface
891
    {
892
        try {
893
            $site = $this->getSiteFinder()
894
                ->getSiteByPageId($pageUid);
895
            return $site;
896
        } catch (SiteNotFoundException $exception) {
897
            return new NullSite();
898
        }
899
    }
900
901
    /**
902
     * @param Site $site
903
     * @return PageSlugCandidateProvider
904
     */
905
    public function getSlugCandidateProvider(Site $site): PageSlugCandidateProvider
906
    {
907
        $context = GeneralUtility::makeInstance(Context::class);
908
        return GeneralUtility::makeInstance(
909
            PageSlugCandidateProvider::class,
910
            $context,
911
            $site,
912
            null
913
        );
914
    }
915
916
    /**
917
     * Convert the base string into a URI object
918
     *
919
     * @param string $base
920
     * @return UriInterface|null
921
     */
922 1
    public function convertStringIntoUri(string $base): ?UriInterface
923
    {
924
        try {
925
            /* @var Uri $uri */
926 1
            $uri = GeneralUtility::makeInstance(
927 1
                Uri::class,
928 1
                $base
929
            );
930
931 1
            return $uri;
932
        } catch (\InvalidArgumentException $argumentException) {
933
            return null;
934
        }
935
    }
936
937
    /**
938
     * In order to search for a path, a possible language prefix need to remove
939
     *
940
     * @param SiteLanguage $language
941
     * @param string $path
942
     * @return string
943
     */
944
    public function stripLanguagePrefixFromPath(SiteLanguage $language, string $path): string
945
    {
946
        if ($language->getBase()->getPath() === '/') {
947
            return $path;
948
        }
949
950
        $pathLength = mb_strlen($language->getBase()->getPath());
951
952
        $path = mb_substr($path, $pathLength, mb_strlen($path) - $pathLength);
953
        if (mb_substr($path, 0, 1) !== '/') {
954
            $path = '/' . $path;
955
        }
956
957
        return $path;
958
    }
959
960
    /**
961
     * Enrich the current query Params with data from path information
962
     *
963
     * @param ServerRequestInterface $request
964
     * @param array $arguments
965
     * @param array $parameters
966
     * @return ServerRequestInterface
967
     */
968 3
    public function addPathArgumentsToQuery(
969
        ServerRequestInterface $request,
970
        array $arguments,
971
        array $parameters
972
    ): ServerRequestInterface {
973 3
        $queryParams = $request->getQueryParams();
974 3
        foreach ($arguments as $fieldName => $queryPath) {
975
            // Skip if there is no parameter
976 3
            if (!isset($parameters[$fieldName])) {
977
                continue;
978
            }
979 3
            $pathElements = explode('/', $queryPath);
980
981 3
            if (!empty($this->pluginNamespace)) {
982 3
                array_unshift($pathElements, $this->pluginNamespace);
983
            }
984
985 3
            $queryParams = $this->processUriPathArgument(
986 3
                $queryParams,
987 3
                $fieldName,
988 3
                $parameters,
989 3
                $pathElements
990
            );
991
        }
992
993 3
        return $request->withQueryParams($queryParams);
994
    }
995
996
    /**
997
     * Check if given argument is a mapping argument
998
     *
999
     * @param string $facetName
1000
     * @return bool
1001
     */
1002
    public function isMappingArgument(string $facetName): bool
1003
    {
1004
        $map = $this->getQueryParameterMap();
1005
        if (isset($map[$facetName]) && $this->shouldMaskQueryParameter()) {
1006
            return true;
1007
        }
1008
1009
        return false;
1010
    }
1011
1012
    /**
1013
     * Check if given facet type is an path argument
1014
     *
1015
     * @param string $facetName
1016
     * @return bool
1017
     */
1018
    public function isPathArgument(string $facetName): bool
1019
    {
1020
        return isset($this->pathArguments[$facetName]);
1021
    }
1022
1023
    /**
1024
     * @param string $variable
1025
     * @return string
1026
     */
1027
    public function reviewVariable(string $variable): string
1028
    {
1029
        if (!$this->containsFacetAndValueSeparator((string)$variable)) {
1030
            return $variable;
1031
        }
1032
1033
        $separator = $this->detectFacetAndValueSeparator((string)$variable);
1034
        [$type, $value] = explode($separator, $variable, 2);
1035
1036
        return $this->isMappingArgument($type) ? $value : $variable;
1037
    }
1038
1039
    /**
1040
     * Remove type prefix from filter
1041
     *
1042
     * @param array $variables
1043
     * @return array
1044
     */
1045
    public function reviseFilterVariables(array $variables): array
1046
    {
1047
        $newVariables = [];
1048
        foreach ($variables as $key => $value) {
1049
            $matches = [];
1050
            if (!preg_match('/###' . $this->getPluginNamespace() . ':filter:\d+:(.+?)###/', $key, $matches)) {
1051
                $newVariables[$key] = $value;
1052
                continue;
1053
            }
1054
            if (!$this->isMappingArgument($matches[1]) && !$this->isPathArgument($matches[1])) {
1055
                $newVariables[$key] = $value;
1056
                continue;
1057
            }
1058
            $separator = $this->detectFacetAndValueSeparator((string)$value);
1059
            $parts = explode($separator, $value);
1060
1061
            do {
1062
                if ($parts[0] === $matches[1]) {
1063
                    array_shift($parts);
1064
                }
1065
            } while ($parts[0] === $matches[1]);
1066
1067
            $newVariables[$key] = implode($separator, $parts);
1068
        }
1069
1070
        return $newVariables;
1071
    }
1072
1073
    /**
1074
     * Converts path segment information into query parameters
1075
     *
1076
     * Example:
1077
     * /products/household
1078
     *
1079
     * tx_solr:
1080
     *      filter:
1081
     *          - type:household
1082
     *
1083
     * @param array $queryParams
1084
     * @param string $fieldName
1085
     * @param array $parameters
1086
     * @param array $pathElements
1087
     * @return array
1088
     */
1089
    protected function processUriPathArgument(
1090
        array $queryParams,
1091
        string $fieldName,
1092
        array $parameters,
1093
        array $pathElements
1094
    ): array {
1095
        $queryKey = array_shift($pathElements);
1096
        $queryKey = (string)$queryKey;
1097
1098
        $tmpQueryKey = $queryKey;
1099
        if (strpos($queryKey, '-') !== false) {
1100
            [$tmpQueryKey, $filterName] = explode('-', $tmpQueryKey, 2);
1101
        }
1102
        if (!isset($queryParams[$tmpQueryKey]) || $queryParams[$tmpQueryKey] === null) {
1103
            $queryParams[$tmpQueryKey] = [];
1104
        }
1105
1106
        if (strpos($queryKey, '-') !== false) {
1107
            [$queryKey, $filterName] = explode('-', $queryKey, 2);
1108
            // explode multiple values
1109
            $values = $this->pathFacetStringToArray($parameters[$fieldName], false);
1110
            sort($values);
1111
1112
            // @TODO: Support URL data bag
1113
            foreach ($values as $value) {
1114
                $value = $this->urlFacetPathService->applyCharacterMap($value);
1115
                $queryParams[$queryKey][] = $filterName . ':' . $value;
1116
            }
1117
        } else {
1118
            $queryParams[$queryKey] = $this->processUriPathArgument(
1119
                $queryParams[$queryKey],
1120
                $fieldName,
1121
                $parameters,
1122
                $pathElements
1123
            );
1124
        }
1125
1126
        return $queryParams;
1127
    }
1128
1129
    /**
1130
     * Return site matcher
1131
     *
1132
     * @return SiteMatcher
1133
     */
1134
    public function getSiteMatcher(): SiteMatcher
1135
    {
1136
        return GeneralUtility::makeInstance(SiteMatcher::class, $this->getSiteFinder());
1137
    }
1138
1139
    /**
1140
     * Returns the site finder
1141
     *
1142
     * @return SiteFinder|null
1143
     */
1144
    protected function getSiteFinder(): ?SiteFinder
1145
    {
1146
        return GeneralUtility::makeInstance(SiteFinder::class);
1147
    }
1148
}
1149