Passed
Pull Request — release-11.2.x (#3604)
by Markus
32:12 queued 27:55
created

SolrRoutingMiddleware::injectRoutingService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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