Passed
Pull Request — master (#2755)
by Rafael
03:55
created

RoutingService::withSettings()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 4
c 2
b 0
f 0
dl 0
loc 6
ccs 0
cts 6
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
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 35
    public function __construct(array $settings = [], string $pluginNamespace = self::PLUGIN_NAMESPACE)
96
    {
97 35
        $this->settings = $settings;
98 35
        $this->pluginNamespace = $pluginNamespace;
99 35
        if (empty($this->pluginNamespace)) {
100
            $this->pluginNamespace = self::PLUGIN_NAMESPACE;
101
        }
102 35
        $this->initUrlFacetService();
103 35
    }
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 35
    public function reset(): RoutingService
165
    {
166 35
        $this->settings = [];
167 35
        $this->pathArguments = [];
168 35
        $this->pluginNamespace = self::PLUGIN_NAMESPACE;
169 35
        return $this;
170
    }
171
172
    /**
173
     * Initialize url facet services for different types
174
     *
175
     * @return $this
176
     */
177 35
    protected function initUrlFacetService(): RoutingService
178
    {
179 35
        $this->urlFacetPathService = new UrlFacetService('path', $this->settings);
180 35
        $this->urlFacetQueryService = new UrlFacetService('query', $this->settings);
181
182 35
        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
        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...
272
            $queryValues[$i] = urldecode($queryValues[$i]);
273
            if ($this->containsFacetAndValueSeparator((string)$queryValues[$i])) {
274
                [$facetName, $facetValue] = explode(
275
                    $this->detectFacetAndValueSeparator((string)$queryValues[$i]),
276
                    (string)$queryValues[$i],
277
                    2
278
                );
279
280
                if ($this->isPathArgument((string)$facetName)) {
281
                    $queryValues[$i] = $facetValue;
282
                }
283
284
            }
285
            $queryValues[$i] = $this->urlFacetPathService->applyCharacterMap($queryValues[$i]);
286
        }
287
288
        $queryValues = array_unique($queryValues);
289
        $queryValues = array_map([$this->urlFacetPathService, 'encodeSingleValue'], $queryValues);
290
        sort($queryValues);
291
        $pathSegments[] = implode(
292
            $this->urlFacetPathService->getMultiValueSeparator(),
293
            $queryValues
294
        );
295
        return implode('/', $pathSegments);
296
    }
297
298
    /**
299
     * This method checks if the query parameter should be masked.
300
     *
301
     * @return bool
302
     */
303
    public function shouldMaskQueryParameter(): bool
304
    {
305
        if (!isset($this->settings['query']['mask']) ||
306
            !(bool)$this->settings['query']['mask']) {
307
            return false;
308
        }
309
310
        $targetFields = $this->getQueryParameterMap();
311
312
        return !empty($targetFields);
313
    }
314
315
    /**
316
     * Masks Solr filter inside of the query parameters
317
     *
318
     * @param array $queryParams
319
     * @return array
320
     */
321
    public function maskQueryParameters(array $queryParams): array
322
    {
323
        if (!$this->shouldMaskQueryParameter()) {
324
            return $queryParams;
325
        }
326
327
        if (!isset($queryParams[$this->getPluginNamespace()])) {
328
            $this->logger
329
                ->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

329
                ->/** @scrutinizer ignore-call */ 
330
                  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...
330
            return $queryParams;
331
        }
332
333
        if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) {
334
            $this->logger
335
                ->info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace());
336
            return $queryParams;
337
        }
338
        $queryParameterMap = $this->getQueryParameterMap();
339
        $newQueryParams = $queryParams;
340
341
        $newFilterArray = [];
342
        foreach ($newQueryParams[$this->getPluginNamespace()]['filter'] as $queryParamName => $queryParamValue) {
343
            $defaultSeparator = $this->detectFacetAndValueSeparator((string)$queryParamValue);
344
            [$facetName, $facetValue] = explode($defaultSeparator, $queryParamValue, 2);
345
            $keep = false;
346
            if (isset($queryParameterMap[$facetName]) &&
347
                isset($newQueryParams[$queryParameterMap[$facetName]])) {
348
                $this->logger->error(
349
                    'Mask error: Facet "' . $facetName . '" as "' . $queryParameterMap[$facetName] .
350
                    '" already in query!'
351
                );
352
                $keep = true;
353
            }
354
            if (!isset($queryParameterMap[$facetName]) || $keep) {
355
                $newFilterArray[] = $queryParamValue;
356
                continue;
357
            }
358
359
            $newQueryParams[$queryParameterMap[$facetName]] = $facetValue;
360
        }
361
362
        $newQueryParams[$this->getPluginNamespace()]['filter'] = $newFilterArray;
363
364
        return $this->cleanUpQueryParameters($newQueryParams);
365
    }
366
367
    /**
368
     * Unmask incoming parameters if needed
369
     *
370
     * @param array $queryParams
371
     * @return array
372
     */
373
    public function unmaskQueryParameters(array $queryParams): array
374
    {
375
        if (!$this->shouldMaskQueryParameter()) {
376
            return $queryParams;
377
        }
378
379
        /*
380
         * The array $queryParameterMap contains the mapping of
381
         * facet name to new url name. In order to unmask we need to switch key and values.
382
         */
383
        $queryParameterMap = $this->getQueryParameterMap();
384
        $queryParameterMapSwitched = [];
385
        foreach ($queryParameterMap as $value => $key) {
386
            $queryParameterMapSwitched[$key] = $value;
387
        }
388
389
        $newQueryParams = [];
390
        foreach ($queryParams as $queryParamName => $queryParamValue) {
391
            // A merge is needed!
392
            if (!isset($queryParameterMapSwitched[$queryParamName])) {
393
                if (isset($newQueryParams[$queryParamName])) {
394
                    $newQueryParams[$queryParamName] = array_merge_recursive(
395
                        $newQueryParams[$queryParamName],
396
                        $queryParamValue
397
                    );
398
                } else {
399
                    $newQueryParams[$queryParamName] = $queryParamValue;
400
                }
401
                continue;
402
            }
403
            if (!isset($newQueryParams[$this->getPluginNamespace()])) {
404
                $newQueryParams[$this->getPluginNamespace()] = [];
405
            }
406
            if (!isset($newQueryParams[$this->getPluginNamespace()]['filter'])) {
407
                $newQueryParams[$this->getPluginNamespace()]['filter'] = [];
408
            }
409
410
            $newQueryParams[$this->getPluginNamespace()]['filter'][] =
411
                $queryParameterMapSwitched[$queryParamName] . ':' . $queryParamValue;
412
        }
413
414
        return $this->cleanUpQueryParameters($newQueryParams);
415
    }
416
417
    /**
418
     * This method check if the query parameters should be touched or not.
419
     *
420
     * There are following requirements:
421
     * - Masking is activated and the mal is valid or
422
     * - Concat is activated
423
     *
424
     * @return bool
425
     */
426
    public function shouldConcatQueryParameters(): bool
427
    {
428
        /*
429
         * The concat will activate automatically if parameters should be masked.
430
         * This solution is less complex since not every mapping parameter needs to be tested
431
         */
432
        if ($this->shouldMaskQueryParameter()) {
433
            return true;
434
        }
435
436
        return isset($this->settings['query']['concat']) && (bool)$this->settings['query']['concat'];
437
    }
438
439
    /**
440
     * Returns the query parameter map
441
     *
442
     * Note TYPO3 core query arguments removed from the configured map!
443
     *
444
     * @return array
445
     */
446
    public function getQueryParameterMap(): array
447
    {
448
        if (!isset($this->settings['query']['map']) ||
449
            !is_array($this->settings['query']['map']) ||
450
            empty($this->settings['query']['map'])) {
451
            return [];
452
        }
453
        // TODO: Test if there is more than one value!
454
        $self = $this;
455
        return array_filter(
456
            $this->settings['query']['map'],
457
            function($value) use ($self) {
458
                return !$self->isCoreParameter($value);
459
            }
460
        );
461
    }
462
463
    /**
464
     * Group all filter values together and concat e
465
     * Note: this will just handle filter values
466
     *
467
     * IN:
468
     * tx_solr => [
469
     *   filter => [
470
     *      color:red
471
     *      product:candy
472
     *      color:blue
473
     *      taste:sour
474
     *   ]
475
     * ]
476
     *
477
     * OUT:
478
     * tx_solr => [
479
     *   filter => [
480
     *      color:blue,red
481
     *      product:candy
482
     *      taste:sour
483
     *   ]
484
     * ]
485
     * @param array $queryParams
486
     * @return array
487
     */
488
    public function concatQueryParameter(array $queryParams = []): array
489
    {
490
        if (!$this->shouldConcatQueryParameters()) {
491
            return $queryParams;
492
        }
493
494
        if (!isset($queryParams[$this->getPluginNamespace()])) {
495
            $this->logger
496
                ->error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace());
497
            return $queryParams;
498
        }
499
500
        if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) {
501
            $this->logger
502
                ->info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace());
503
            return $queryParams;
504
        }
505
506
        $queryParams[$this->getPluginNamespace()]['filter'] =
507
            $this->concatFilterValues($queryParams[$this->getPluginNamespace()]['filter']);
508
509
        return $this->cleanUpQueryParameters($queryParams);
510
    }
511
512
    /**
513
     * This method expect a filter array that should be concat instead of the whole query
514
     *
515
     * @param array $filterArray
516
     * @return array
517
     */
518
    public function concatFilterValues(array $filterArray): array
519
    {
520
        if (empty($filterArray) || !$this->shouldConcatQueryParameters()) {
521
            return $filterArray;
522
        }
523
524
        $queryParameterMap = $this->getQueryParameterMap();
525
        $newFilterArray = [];
526
        $defaultSeparator = $this->detectFacetAndValueSeparator((string)$filterArray[0]);
527
        // Collect parameter names and rename parameter if required
528
        foreach ($filterArray as $set) {
529
            $separator = $this->detectFacetAndValueSeparator((string)$set);
530
            [$facetName, $facetValue] = explode($separator, $set, 2);
531
            if (isset($queryParameterMap[$facetName])) {
532
                $facetName = $queryParameterMap[$facetName];
533
            }
534
            if (!isset($newFilterArray[$facetName])) {
535
                $newFilterArray[$facetName] = [$facetValue];
536
            } else {
537
                $newFilterArray[$facetName][] = $facetValue;
538
            }
539
        }
540
541
        foreach ($newFilterArray as $facetName => $facetValues) {
542
            $newFilterArray[$facetName] = $facetName . $defaultSeparator . $this->queryParameterFacetsToString($facetValues);
543
        }
544
545
        return array_values($newFilterArray);
546
    }
547
548
    /**
549
     * Inflate given query parameters if configured
550
     * Note: this will just combine filter values
551
     *
552
     * IN:
553
     * tx_solr => [
554
     *   filter => [
555
     *      color:blue,red
556
     *      product:candy
557
     *      taste:sour
558
     *   ]
559
     * ]
560
     *
561
     * OUT:
562
     * tx_solr => [
563
     *   filter => [
564
     *      color:red
565
     *      product:candy
566
     *      color:blue
567
     *      taste:sour
568
     *   ]
569
     * ]
570
     *
571
     * @param array $queryParams
572
     * @return array
573
     */
574
    public function inflateQueryParameter(array $queryParams = []): array
575
    {
576
        if (!$this->shouldConcatQueryParameters()) {
577
            return $queryParams;
578
        }
579
580
        if (!isset($queryParams[$this->getPluginNamespace()])) {
581
            $queryParams[$this->getPluginNamespace()] = [];
582
        }
583
584
        if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) {
585
            $queryParams[$this->getPluginNamespace()]['filter'] = [];
586
        }
587
588
        $newQueryParams = [];
589
        foreach ($queryParams[$this->getPluginNamespace()]['filter'] as $set) {
590
            $separator = $this->detectFacetAndValueSeparator((string)$set);
591
            [$facetName, $facetValuesString] = explode($separator, $set, 2);
592
            $facetValues = [$facetValuesString];
0 ignored issues
show
Unused Code introduced by
The assignment to $facetValues is dead and can be removed.
Loading history...
593
            $facetValues = explode($this->urlFacetQueryService->getMultiValueSeparator(), $facetValuesString);
594
595
            /**
596
             * A facet value could contain the multi value separator. This value is masked in order to
597
             * avoid problems during separation of the values (line above).
598
             *
599
             * After splitting the values, the character inside the value need to be restored
600
             *
601
             * @see RoutingService::queryParameterFacetsToString
602
             */
603
            $facetValues = array_map([$this->urlFacetQueryService, 'decodeSingleValue'], $facetValues);
604
605
            foreach ($facetValues as $facetValue) {
606
                $newQueryParams[] = $facetName . $separator . $facetValue;
607
            }
608
        }
609
        $queryParams[$this->getPluginNamespace()]['filter'] = array_values($newQueryParams);
610
611
        return $this->cleanUpQueryParameters($queryParams);
612
    }
613
614
    /**
615
     * Cleanup the query parameters, to avoid empty solr arguments
616
     *
617
     * @param array $queryParams
618
     * @return array
619
     */
620
    public function cleanUpQueryParameters(array $queryParams): array
621
    {
622
        if (empty($queryParams[$this->getPluginNamespace()]['filter'])) {
623
            unset($queryParams[$this->getPluginNamespace()]['filter']);
624
        }
625
626
        if (empty($queryParams[$this->getPluginNamespace()])) {
627
            unset($queryParams[$this->getPluginNamespace()]);
628
        }
629
        return $queryParams;
630
    }
631
632
    /**
633
     * Builds a string out of multiple facet values
634
     *
635
     * A facet value could contain the multi value separator. This value have to masked in order to
636
     * avoid problems during separation of the values later.
637
     *
638
     * This mask have to apply before contact the values
639
     *
640
     * @param array $facets
641
     * @return string
642
     */
643
    public function queryParameterFacetsToString(array $facets): string
644
    {
645
        $facets = array_map([$this->urlFacetQueryService, 'encodeSingleValue'], $facets);
646
        sort($facets);
647
        return implode($this->urlFacetQueryService->getMultiValueSeparator(), $facets);
648
    }
649
650
    /**
651
     * Returns the string which separates the facet from the value
652
     *
653
     * @param string $facetWithValue
654
     * @return string
655
     */
656
    public function detectFacetAndValueSeparator(string $facetWithValue): string
657
    {
658
        $separator = ':';
659
        if (mb_strpos($facetWithValue, '%3A') !== false) {
660
            $separator = '%3A';
661
        }
662
663
        return $separator;
664
    }
665
666
    /**
667
     * Check if given facet value combination contains a separator
668
     *
669
     * @param string $facetWithValue
670
     * @return bool
671
     */
672
    public function containsFacetAndValueSeparator(string $facetWithValue): bool
673
    {
674
        if (mb_strpos($facetWithValue, ':') === false && mb_strpos($facetWithValue, '%3A') === false) {
675
            return false;
676
        }
677
678
        return true;
679
    }
680
681
    /**
682
     * Cleanup facet values (strip type if needed)
683
     *
684
     * @param array $facetValues
685
     * @return array
686
     */
687
    public function cleanupFacetValues(array $facetValues): array
688
    {
689
        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...
690
            if (!$this->containsFacetAndValueSeparator((string)$facetValues[$i])) {
691
                continue;
692
            }
693
694
            $separator = $this->detectFacetAndValueSeparator((string)$facetValues[$i]);
695
            [$type, $value] = explode($separator, $facetValues[$i]);
696
697
            if ($this->isMappingArgument($type) || $this->isPathArgument($type)) {
698
                $facetValues[$i] = $value;
699
            }
700
        }
701
        return $facetValues;
702
    }
703
704
    /**
705
     * Builds a string out of multiple facet values
706
     *
707
     * @param array $facets
708
     * @return string
709
     */
710
    public function pathFacetsToString(array $facets): string
711
    {
712
        $facets = $this->cleanupFacetValues($facets);
713
        sort($facets);
714
        $facets = array_map([$this->urlFacetPathService, 'applyCharacterMap'], $facets);
715
        $facets = array_map([$this->urlFacetPathService, 'encodeSingleValue'], $facets);
716
        return implode($this->urlFacetPathService->getMultiValueSeparator(), $facets);
717
    }
718
719
    /**
720
     * Builds a string out of multiple facet values
721
     *
722
     * @param array $facets
723
     * @return string
724
     */
725
    public function facetsToString(array $facets): string
726
    {
727
        $facets = $this->cleanupFacetValues($facets);
728
        sort($facets);
729
        return implode($this->getDefaultMultiValueSeparator(), $facets);
730
    }
731
732
    /**
733
     * Builds a string out of multiple facet values
734
     *
735
     * This method is used in two different situation
736
     *  1. Middleware: Here the values should not be decoded
737
     *  2. Within the event listener CachedPathVariableModifier
738
     *
739
     * @param string $facets
740
     * @param bool $decode
741
     * @return array
742
     */
743
    public function pathFacetStringToArray(string $facets, bool $decode = true): array
744
    {
745
        $facetString = $this->urlFacetPathService->applyCharacterMap($facets);
746
        $facets = explode($this->urlFacetPathService->getMultiValueSeparator(), $facetString);
747
        if (!$decode) {
748
            return $facets;
749
        }
750
        return array_map([$this->urlFacetPathService, 'decodeSingleValue'], $facets);
751
    }
752
753
    /**
754
     * Returns the multi value separator
755
     * @return string
756
     */
757
    public function getDefaultMultiValueSeparator(): string
758
    {
759
        return $this->settings['multiValueSeparator'] ?? ',';
760
    }
761
762
    /**
763
     * Find a enhancer configuration by a given page id
764
     *
765
     * @param int $pageUid
766
     * @return array
767
     */
768 35
    public function fetchEnhancerByPageUid(int $pageUid): array
769
    {
770 35
        $site = $this->findSiteByUid($pageUid);
771 35
        if ($site instanceof NullSite) {
772
            return [];
773
        }
774
775 35
        return $this->fetchEnhancerInSiteConfigurationByPageUid(
776 35
            $site,
777
            $pageUid
778
        );
779
    }
780
781
    /**
782
     * Returns the route enhancer configuration by given site and page uid
783
     *
784
     * @param Site $site
785
     * @param int $pageUid
786
     * @return array
787
     */
788 35
    public function fetchEnhancerInSiteConfigurationByPageUid(Site $site, int $pageUid): array
789
    {
790 35
        $configuration = $site->getConfiguration();
791 35
        if (empty($configuration['routeEnhancers']) || !is_array($configuration['routeEnhancers'])) {
792 35
            return [];
793
        }
794
        $result = [];
795
        foreach ($configuration['routeEnhancers'] as $routing => $settings) {
796
            // Not the page we are looking for
797
            if (isset($settings['limitToPages']) &&
798
                is_array($settings['limitToPages']) &&
799
                !in_array($pageUid, $settings['limitToPages'])) {
800
                continue;
801
            }
802
803
            if (empty($settings) || !isset($settings['type']) ||
804
                !$this->isRouteEnhancerForSolr((string)$settings['type'])
805
            ) {
806
                continue;
807
            }
808
            $result[] = $settings;
809
        }
810
811
        return $result;
812
    }
813
814
    /**
815
     * Add heading slash to given slug
816
     *
817
     * @param string $slug
818
     * @return string
819
     */
820
    public function cleanupHeadingSlash(string $slug): string
821
    {
822
        if (mb_substr($slug, 0, 1) !== '/') {
823
            return '/' . $slug;
824
        } else if (mb_substr($slug, 0, 2) === '//') {
825
            return mb_substr($slug, 1, mb_strlen($slug) - 1);
826
        }
827
828
        return $slug;
829
    }
830
831
    /**
832
     * Add heading slash to given slug
833
     *
834
     * @param string $slug
835
     * @return string
836
     */
837
    public function addHeadingSlash(string $slug): string
838
    {
839
        if (mb_substr($slug, 0, 1) === '/') {
840
            return $slug;
841
        }
842
843
        return '/' . $slug;
844
    }
845
846
    /**
847
     * Remove heading slash from given slug
848
     *
849
     * @param string $slug
850
     * @return string
851
     */
852
    public function removeHeadingSlash(string $slug): string
853
    {
854
        if (mb_substr($slug, 0, 1) !== '/') {
855
            return $slug;
856
        }
857
858
        return mb_substr($slug, 1, mb_strlen($slug) - 1);
859
    }
860
861
    /**
862
     * Retrieve the site by given UID
863
     *
864
     * @param int $pageUid
865
     * @return SiteInterface
866
     */
867 35
    public function findSiteByUid(int $pageUid): SiteInterface
868
    {
869
        try {
870 35
            $site = $this->getSiteFinder()
871 35
                ->getSiteByPageId($pageUid);
872 35
            return $site;
873
        } catch (SiteNotFoundException $exception) {
874
            return new NullSite();
875
        }
876
    }
877
878
    /**
879
     * @param Site $site
880
     * @return PageSlugCandidateProvider
881
     */
882
    public function getSlugCandidateProvider(Site $site): PageSlugCandidateProvider
883
    {
884
        $context = GeneralUtility::makeInstance(Context::class);
885
        return GeneralUtility::makeInstance(
886
            PageSlugCandidateProvider::class,
887
            $context,
888
            $site,
889
            null
890
        );
891
    }
892
893
    /**
894
     * Convert the base string into a URI object
895
     *
896
     * @param string $base
897
     * @return UriInterface|null
898
     */
899
    public function convertStringIntoUri(string $base): ?UriInterface
900
    {
901
        try {
902
            /* @var Uri $uri */
903
            $uri = GeneralUtility::makeInstance(
904
                Uri::class,
905
                $base
906
            );
907
908
            return $uri;
909
        } catch (\InvalidArgumentException $argumentException) {
910
            return null;
911
        }
912
    }
913
914
    /**
915
     * In order to search for a path, a possible language prefix need to remove
916
     *
917
     * @param SiteLanguage $language
918
     * @param string $path
919
     * @return string
920
     */
921
    public function stripLanguagePrefixFromPath(SiteLanguage $language, string $path): string
922
    {
923
        if ($language->getBase()->getPath() === '/') {
924
            return $path;
925
        }
926
927
        $pathLength = mb_strlen($language->getBase()->getPath());
928
929
        $path = mb_substr($path, $pathLength, mb_strlen($path) - $pathLength);
930
        if (mb_substr($path, 0, 1) !== '/') {
931
            $path = '/' . $path;
932
        }
933
934
        return $path;
935
    }
936
937
    /**
938
     * Enrich the current query Params with data from path information
939
     *
940
     * @param ServerRequestInterface $request
941
     * @param array $arguments
942
     * @param array $parameters
943
     * @return ServerRequestInterface
944
     */
945
    public function addPathArgumentsToQuery(
946
        ServerRequestInterface $request,
947
        array $arguments,
948
        array $parameters
949
    ): ServerRequestInterface {
950
        $queryParams = $request->getQueryParams();
951
        foreach ($arguments as $fieldName => $queryPath) {
952
            // Skip if there is no parameter
953
            if (!isset($parameters[$fieldName])) {
954
                continue;
955
            }
956
            $pathElements = explode('/', $queryPath);
957
958
            if (!empty($this->pluginNamespace)) {
959
                array_unshift($pathElements, $this->pluginNamespace);
960
            }
961
962
            $queryParams = $this->processUriPathArgument(
963
                $queryParams,
964
                $fieldName,
965
                $parameters,
966
                $pathElements
967
            );
968
        }
969
970
        return $request->withQueryParams($queryParams);
971
    }
972
973
    /**
974
     * Check if given argument is a mapping argument
975
     *
976
     * @param string $facetName
977
     * @return bool
978
     */
979
    public function isMappingArgument(string $facetName): bool
980
    {
981
        $map = $this->getQueryParameterMap();
982
        if (isset($map[$facetName]) && $this->shouldMaskQueryParameter()) {
983
            return true;
984
        }
985
986
        return false;
987
    }
988
989
    /**
990
     * Check if given facet type is an path argument
991
     *
992
     * @param string $facetName
993
     * @return bool
994
     */
995
    public function isPathArgument(string $facetName): bool
996
    {
997
        return isset($this->pathArguments[$facetName]);
998
    }
999
1000
    /**
1001
     * @param string $variable
1002
     * @return string
1003
     */
1004
    public function reviewVariable(string $variable): string
1005
    {
1006
        if (!$this->containsFacetAndValueSeparator((string)$variable)) {
1007
            return $variable;
1008
        }
1009
1010
        $separator = $this->detectFacetAndValueSeparator((string)$variable);
1011
        [$type, $value] = explode($separator, $variable, 2);
1012
1013
        return $this->isMappingArgument($type) ? $value : $variable;
1014
    }
1015
1016
    /**
1017
     * Remove type prefix from filter
1018
     *
1019
     * @param array $variables
1020
     * @return array
1021
     */
1022
    public function reviseFilterVariables(array $variables): array
1023
    {
1024
        $newVariables = [];
1025
        foreach ($variables as $key => $value) {
1026
            $matches = [];
1027
            if (!preg_match('/###' . $this->getPluginNamespace() . ':filter:\d+:(.+?)###/', $key, $matches)) {
1028
                $newVariables[$key] = $value;
1029
                continue;
1030
            }
1031
            if (!$this->isMappingArgument($matches[1]) && !$this->isPathArgument($matches[1])) {
1032
                $newVariables[$key] = $value;
1033
                continue;
1034
            }
1035
            $separator = $this->detectFacetAndValueSeparator((string)$value);
1036
            $parts = explode($separator, $value);
1037
1038
            do {
1039
                if ($parts[0] === $matches[1]) {
1040
                    array_shift($parts);
1041
                }
1042
            } while ($parts[0] === $matches[1]);
1043
1044
            $newVariables[$key] = implode($separator, $parts);
1045
        }
1046
1047
        return $newVariables;
1048
    }
1049
1050
    /**
1051
     * Converts path segment information into query parameters
1052
     *
1053
     * Example:
1054
     * /products/household
1055
     *
1056
     * tx_solr:
1057
     *      filter:
1058
     *          - type:household
1059
     *
1060
     * @param array $queryParams
1061
     * @param string $fieldName
1062
     * @param array $parameters
1063
     * @param array $pathElements
1064
     * @return array
1065
     */
1066
    protected function processUriPathArgument(
1067
        array $queryParams,
1068
        string $fieldName,
1069
        array $parameters,
1070
        array $pathElements
1071
    ): array {
1072
        $queryKey = array_shift($pathElements);
1073
        $queryKey = (string)$queryKey;
1074
1075
        $tmpQueryKey = $queryKey;
1076
        if (strpos($queryKey, '-') !== false) {
1077
            [$tmpQueryKey, $filterName] = explode('-', $tmpQueryKey, 2);
1078
        }
1079
        if (!isset($queryParams[$tmpQueryKey]) || $queryParams[$tmpQueryKey] === null) {
1080
            $queryParams[$tmpQueryKey] = [];
1081
        }
1082
1083
        if (strpos($queryKey, '-') !== false) {
1084
            [$queryKey, $filterName] = explode('-', $queryKey, 2);
1085
            // explode multiple values
1086
            $values = $this->pathFacetStringToArray($parameters[$fieldName], false);
1087
            sort($values);
1088
1089
            // @TODO: Support URL data bag
1090
            foreach ($values as $value) {
1091
                $value = $this->urlFacetPathService->applyCharacterMap($value);
1092
                $queryParams[$queryKey][] = $filterName . ':' . $value;
1093
            }
1094
        } else {
1095
            $queryParams[$queryKey] = $this->processUriPathArgument(
1096
                $queryParams[$queryKey],
1097
                $fieldName,
1098
                $parameters,
1099
                $pathElements
1100
            );
1101
        }
1102
1103
        return $queryParams;
1104
    }
1105
1106
    /**
1107
     * Return site matcher
1108
     *
1109
     * @return SiteMatcher
1110
     */
1111
    public function getSiteMatcher(): SiteMatcher
1112
    {
1113
        return GeneralUtility::makeInstance(SiteMatcher::class, $this->getSiteFinder());
1114
    }
1115
1116
    /**
1117
     * Returns the site finder
1118
     *
1119
     * @return SiteFinder|null
1120
     */
1121 35
    protected function getSiteFinder(): ?SiteFinder
1122
    {
1123 35
        return GeneralUtility::makeInstance(SiteFinder::class);
1124
    }
1125
}
1126