Passed
Push — release-11.2.x ( 9027bf...093924 )
by Markus
12:13
created

SolrRoutingMiddleware   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Test Coverage

Coverage 11.35%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 32
eloc 153
c 2
b 0
f 0
dl 0
loc 343
ccs 16
cts 141
cp 0.1135
rs 9.84

7 Methods

Rating   Name   Duplication   Size   Complexity  
B retrievePageInformation() 0 73 8
A getEnhancerConfiguration() 0 12 2
B extractParametersFromUriPath() 0 67 9
A configure() 0 5 1
A injectRoutingService() 0 3 1
A getRoutingService() 0 12 2
B process() 0 87 9
1
<?php
2
namespace ApacheSolrForTypo3\Solr\Middleware;
3
4
/*
5
 * This file is part of the TYPO3 CMS project.
6
 *
7
 * It is free software; you can redistribute it and/or modify it under
8
 * the terms of the GNU General Public License, either version 2
9
 * of the License, or any later version.
10
 *
11
 * For the full copyright and license information, please read the
12
 * LICENSE.txt file that was distributed with this source code.
13
 *
14
 * The TYPO3 project - inspiring people to share!
15
 */
16
17
use ApacheSolrForTypo3\Solr\IndexQueue\PageIndexerRequest;
18
use ApacheSolrForTypo3\Solr\Routing\RoutingService;
19
use Psr\Http\Message\ResponseInterface;
20
use Psr\Http\Message\ServerRequestInterface;
21
use Psr\Http\Message\UriInterface;
22
use Psr\Http\Server\MiddlewareInterface;
23
use Psr\Http\Server\RequestHandlerInterface;
24
use Psr\Log\LoggerAwareInterface;
25
use Psr\Log\LoggerAwareTrait;
26
use TYPO3\CMS\Core\Routing\SiteRouteResult;
27
use TYPO3\CMS\Core\Site\Entity\NullSite;
28
use TYPO3\CMS\Core\Site\Entity\Site;
29
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
32
/**
33
 * Middleware to create beautiful URLs for Solr
34
 *
35
 * How to use:
36
 * Inside of your extension create following file
37
 * Configuration/RequestMiddlewares.php
38
 *
39
 * return [
40
 *   'frontend' => [
41
 *     'apache-solr-for-typo3/solr-route-enhancer' => [
42
 *       'target' => \ApacheSolrForTypo3\Solr\Middleware\SolrRoutingMiddleware::class,
43
 *       'before' => [
44
 *         'typo3/cms-frontend/site',
45
 *       ]
46
 *     ]
47
 *   ]
48
 * ];
49
 *
50
 * @author Lars Tode <[email protected]>
51
 * @see https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/RequestHandling/Index.html
52
 */
53
class SolrRoutingMiddleware implements MiddlewareInterface, LoggerAwareInterface
54
{
55
    use LoggerAwareTrait;
56
57
    /**
58
     * Solr parameter key
59
     *
60
     * @var string
61
     */
62
    protected $namespace = 'tx_solr';
63
64
    /**
65
     * Settings from enhancer configuration
66
     *
67
     * @var array
68
     */
69
    protected $settings = [];
70
71
    /**
72
     * @var SiteLanguage
73
     */
74
    protected $language = null;
75
76
    /**
77
     * @var RoutingService
78
     */
79
    protected $routingService;
80
81
    /**
82
     * Inject the routing service.
83
     * Used in unit tests too
84
     *
85
     * @param RoutingService $routingService
86
     */
87
    public function injectRoutingService(RoutingService $routingService)
88
    {
89
        $this->routingService = $routingService;
90
    }
91
92
    /**
93
     * Process the request
94
     *
95
     * @param ServerRequestInterface $request
96
     * @param RequestHandlerInterface $handler
97
     * @return ResponseInterface
98
     */
99 1
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
100
    {
101 1
        if (!$request->hasHeader(PageIndexerRequest::SOLR_INDEX_HEADER)) {
102
            return $handler->handle($request);
103
        }
104
105
        /* @var SiteRouteResult $routeResult */
106 1
        $routeResult = $this->getRoutingService()
107 1
            ->getSiteMatcher()
108 1
            ->matchRequest($request);
109
110 1
        $site = $routeResult->getSite();
111
112 1
        if ($site instanceof NullSite) {
113
            return $handler->handle($request);
114
        }
115
116 1
        $this->language = $routeResult->getLanguage();
117
118 1
        if (!($this->language instanceof SiteLanguage)) {
0 ignored issues
show
introduced by
$this->language is always a sub-type of TYPO3\CMS\Core\Site\Entity\SiteLanguage.
Loading history...
119
            return $handler->handle($request);
120
        }
121
122 1
        $page = $this->retrievePageInformation(
123 1
            $request->getUri(),
124
            $site
125
        );
126
127 1
        if ((int)$page['uid'] === 0) {
128
            return $handler->handle($request);
129
        }
130
131 1
        $enhancerConfiguration = $this->getEnhancerConfiguration(
132
            $site,
133 1
            $this->language->getLanguageId() === 0 ? (int)$page['uid'] : (int)$page['l10n_parent']
134
        );
135
136 1
        if ($enhancerConfiguration === null) {
137 1
            return $handler->handle($request);
138
        }
139
140
        $this->configure($enhancerConfiguration);
141
142
        /*
143
         * Take slug path segments and argument from incoming URI
144
         */
145
        [$slug, $parameters] = $this->extractParametersFromUriPath(
146
            $request->getUri(),
147
            $enhancerConfiguration['routePath'],
148
            (string)$page['slug']
149
        );
150
151
        /*
152
         * Convert path arguments to query arguments
153
         */
154
        if (!empty($parameters)) {
155
            $request = $this->getRoutingService()->addPathArgumentsToQuery(
156
                $request,
157
                $enhancerConfiguration['_arguments'],
158
                $parameters
159
            );
160
        }
161
162
        /*
163
         * Replace internal URI with existing site taken from path information
164
         * We removed a possible path segment from the slug, that again needs to attach.
165
         *
166
         * NOTE: TypoScript is not available at this point!
167
         */
168
        $uri = $request->getUri()->withPath(
169
            $this->getRoutingService()->cleanupHeadingSlash(
170
                $this->language->getBase()->getPath() .
171
                (string)$page['slug']
172
            )
173
        );
174
        $request = $request->withUri($uri);
175
        $queryParams = $request->getQueryParams();
176
        $queryParams = $this->getRoutingService()->unmaskQueryParameters($queryParams);
177
        $queryParams = $this->getRoutingService()->inflateQueryParameter($queryParams);
178
179
        // @todo Drop cHash, but need to recalculate
180
        if (array_key_exists('cHash', $queryParams)) {
181
            unset($queryParams['cHash']);
182
        }
183
184
        $request = $request->withQueryParams($queryParams);
185
        return $handler->handle($request);
186
    }
187
188
    /**
189
     * Configures the middleware by enhancer configuration
190
     *
191
     * @param array $enhancerConfiguration
192
     */
193
    protected function configure(array $enhancerConfiguration): void
194
    {
195
        $this->settings = $enhancerConfiguration['solr'];
196
        $this->namespace = $enhancerConfiguration['extensionKey'] ?? $this->namespace;
197
        $this->routingService = null;
198
    }
199
200
    /**
201
     * Retrieve the enhancer configuration for given site
202
     *
203
     * @param Site $site
204
     * @param int $pageUid
205
     * @return array|null
206
     */
207
    protected function getEnhancerConfiguration(Site $site, int $pageUid): ?array
208
    {
209
        $enhancers = $this->getRoutingService()->fetchEnhancerInSiteConfigurationByPageUid(
210
            $site,
211
            $pageUid
212
        );
213
214
        if (empty($enhancers)) {
215
            return null;
216
        }
217
218
        return $enhancers[0];
219
    }
220
221
    /**
222
     * Extract the slug and all arguments from path
223
     *
224
     * @param UriInterface $uri
225
     * @param string $path
226
     * @param string $pageSlug
227
     * @return array
228
     */
229
    protected function extractParametersFromUriPath(UriInterface $uri, string $path, string $pageSlug): array
230
    {
231
        // URI get path returns the path with given language parameter
232
        // The parameter pageSlug itself does not contains the language parameter.
233
        $uriPath = $this->getRoutingService()->stripLanguagePrefixFromPath(
234
            $this->language,
235
            $uri->getPath()
236
        );
237
238
        if ($uriPath === $pageSlug) {
239
            return [
240
                $pageSlug,
241
                []
242
            ];
243
        }
244
245
        // Remove slug from URI path in order the ensure only the arguments left
246
        if (mb_substr($uriPath, 0, mb_strlen($pageSlug) + 1) === $pageSlug . '/') {
247
            $length = mb_strlen($pageSlug) + 1;
248
            $uriPath = mb_substr($uriPath, $length, mb_strlen($uriPath) - $length);
249
        }
250
251
        // Take care the format of configuration and given slug equals
252
        $uriPath = $this->getRoutingService()->removeHeadingSlash($uriPath);
253
        $path = $this->getRoutingService()->removeHeadingSlash($path);
254
255
        // Remove begin
256
        $uriElements = explode('/', $uriPath);
257
        $routeElements = explode('/', $path);
258
        $slugElements = [];
259
        $arguments = [];
260
        $process = true;
261
        /*
262
         * Extract the slug elements, until the the amount of route elements reached
263
         */
264
        do {
265
            if (count($uriElements) > count($routeElements)) {
266
                $slugElements[] = array_shift($uriElements);
267
            } else {
268
                $process = false;
269
            }
270
        } while ($process);
271
272
        if (empty($routeElements[0])) {
273
            array_shift($routeElements);
274
        }
275
        if (empty($uriElements[0])) {
276
            array_shift($uriElements);
277
        }
278
279
        // Extract the values
280
        $uriElementsCount = count($uriElements);
281
        for ($i = 0; $i < $uriElementsCount; $i++) {
282
            // Skip empty elements
283
            if (empty($uriElements[$i])) {
284
                continue;
285
            }
286
287
            $key = substr($routeElements[$i], 1, strlen($routeElements[$i]) - 1);
288
            $key = substr($key, 0, strlen($key) - 1);
289
290
            $arguments[$key] = $uriElements[$i];
291
        }
292
293
        return [
294
            implode('/', $slugElements),
295
            $arguments
296
        ];
297
    }
298
299
    /**
300
     * Retrieve the page uid to filter the route enhancer
301
     *
302
     * @param UriInterface $uri
303
     * @param Site $site
304
     * @return array
305
     */
306
    protected function retrievePageInformation(UriInterface $uri, Site $site): array
307
    {
308
        $path = $this->getRoutingService()->stripLanguagePrefixFromPath(
309
            $this->language,
310
            $uri->getPath()
311
        );
312
        $slugProvider = $this->getRoutingService()->getSlugCandidateProvider($site);
313
        $scan = true;
314
        $page = [];
315
        do {
316
            $items = $slugProvider->getCandidatesForPath(
317
                $path,
318
                $this->language
319
            );
320
            if (empty($items)) {
321
                $this->logger
322
                    ->/** @scrutinizer ignore-call */
323
                    error(
324
                        vsprintf(
325
                            'Could not determine page for slug "%1$s" and language "%2$s". Given path "%3$s"',
326
                            [
327
                                $path,
328
                                $this->language->getTwoLetterIsoCode(),
329
                                $uri->getPath()
330
                            ]
331
                        )
332
                    );
333
                $scan = false;
334
            } elseif (empty($path)) {
335
                $this->logger
336
                    ->/** @scrutinizer ignore-call */
337
                    error(
338
                        vsprintf(
339
                            'Could resolve page by path "%1$s" and language "%2$s".',
340
                            [
341
                                $uri->getPath(),
342
                                $this->language->getTwoLetterIsoCode()
343
                            ]
344
                        )
345
                    );
346
                $scan = false;
347
            } else {
348
                foreach ($items as $item) {
349
                    $this->logger
350
                        ->info(
0 ignored issues
show
Bug introduced by
The method info() 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

350
                        ->/** @scrutinizer ignore-call */ 
351
                          info(

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...
351
                            vsprintf(
352
                                'Path "%1$s" -> slug "%2$s"',
353
                                [
354
                                    $path,
355
                                    $item['slug']
356
                                ]
357
                            )
358
                        );
359
360
                    if ($item['slug'] === $path) {
361
                        $page = $item;
362
                        $scan = false;
363
                        break;
364
                    }
365
                }
366
367
                if ($scan) {
368
                    $elements = explode('/', $path);
369
                    if (empty($elements)) {
370
                        $scan = false;
371
                    } else {
372
                        array_pop($elements);
373
                        $path = implode('/', $elements);
374
                    }
375
                }
376
            }
377
        } while($scan);
378
        return $page;
379
    }
380
381
    /**
382
     * @return RoutingService
383
     */
384
    protected function getRoutingService(): RoutingService
385
    {
386
        if (null === $this->routingService) {
387
            $this->routingService = GeneralUtility::makeInstance(
388
                RoutingService::class,
389
                $this->settings,
390
                $this->namespace
391
            );
392
        } else {
393
            $this->routingService = $this->routingService->withSettings($this->settings);
394
        }
395
        return $this->routingService;
396
    }
397
}
398