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

SolrRoutingMiddleware   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 32
eloc 150
dl 0
loc 338
ccs 0
cts 210
cp 0
rs 9.84
c 2
b 0
f 0

7 Methods

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

318
                    ->/** @scrutinizer ignore-call */ 
319
                      error(

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...
319
                        vsprintf(
320
                            'Could not determine page for slug "%1$s" and language "%2$s". Given path "%3$s"',
321
                            [
322
                                $path,
323
                                $this->language->getTwoLetterIsoCode(),
324
                                $uri->getPath()
325
                            ]
326
                        )
327
                    );
328
                $scan = false;
329
            } elseif (empty($path)) {
330
                $this->logger
331
                    ->error(
332
                        vsprintf(
333
                            'Could resolve page by path "%1$s" and language "%2$s".',
334
                            [
335
                                $uri->getPath(),
336
                                $this->language->getTwoLetterIsoCode()
337
                            ]
338
                        )
339
                    );
340
                $scan = false;
341
            } else {
342
                foreach ($items as $item) {
343
                    $this->logger
344
                        ->info(
345
                            vsprintf(
346
                                'Path "%1$s" -> slug "%2$s"',
347
                                [
348
                                    $path,
349
                                    $item['slug']
350
                                ]
351
                            )
352
                        );
353
354
                    if ($item['slug'] === $path) {
355
                        $page = $item;
356
                        $scan = false;
357
                        break;
358
                    }
359
                }
360
361
                if ($scan) {
362
                    $elements = explode('/', $path);
363
                    if (empty($elements)) {
364
                        $scan = false;
365
                    } else {
366
                        array_pop($elements);
367
                        $path = implode('/', $elements);
368
                    }
369
                }
370
            }
371
        } while($scan);
372
        return $page;
373
    }
374
375
    /**
376
     * @return RoutingService
377
     */
378
    protected function getRoutingService(): RoutingService
379
    {
380
        if (!($this->routingService instanceof RoutingService)) {
0 ignored issues
show
introduced by
$this->routingService is always a sub-type of ApacheSolrForTypo3\Solr\Routing\RoutingService.
Loading history...
381
            $this->routingService = GeneralUtility::makeInstance(
382
                RoutingService::class,
383
                $this->settings,
384
                $this->namespace
385
            );
386
        } else {
387
            $this->routingService = $this->routingService->withSettings($this->settings);
388
        }
389
        return $this->routingService;
390
    }
391
}
392