Completed
Push — master ( 30af4a...256b3e )
by Łukasz
18:25 queued 05:11
created

UrlAliasRouter::supportsObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the UrlAliasRouter class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\MVC\Symfony\Routing;
10
11
use eZ\Publish\API\Repository\LocationService;
12
use eZ\Publish\API\Repository\URLAliasService;
13
use eZ\Publish\API\Repository\ContentService;
14
use eZ\Publish\API\Repository\Values\Content\URLAlias;
15
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
16
use eZ\Publish\API\Repository\Values\Content\Location;
17
use eZ\Publish\Core\MVC\Symfony\View\Manager as ViewManager;
18
use eZ\Publish\Core\MVC\Symfony\Routing\Generator\UrlAliasGenerator;
19
use Symfony\Cmf\Component\Routing\ChainedRouterInterface;
20
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
21
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\Routing\RequestContext;
24
use Psr\Log\LoggerInterface;
25
use Symfony\Component\Routing\RouteCollection;
26
use Symfony\Component\Routing\Route as SymfonyRoute;
27
use Symfony\Component\Routing\Exception\RouteNotFoundException;
28
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
29
use InvalidArgumentException;
30
use LogicException;
31
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
32
33
class UrlAliasRouter implements ChainedRouterInterface, RequestMatcherInterface
34
{
35
    const URL_ALIAS_ROUTE_NAME = 'ez_urlalias';
36
37
    const VIEW_ACTION = 'ez_content:viewAction';
38
39
    /** @var \Symfony\Component\Routing\RequestContext */
40
    protected $requestContext;
41
42
    /** @var \eZ\Publish\API\Repository\LocationService */
43
    protected $locationService;
44
45
    /** @var \eZ\Publish\API\Repository\URLAliasService */
46
    protected $urlAliasService;
47
48
    /** @var \eZ\Publish\API\Repository\ContentService */
49
    protected $contentService;
50
51
    /** @var \eZ\Publish\Core\MVC\Symfony\Routing\Generator\UrlAliasGenerator */
52
    protected $generator;
53
54
    /**
55
     * Holds current root Location id.
56
     *
57
     * @var int|string
58
     */
59
    protected $rootLocationId;
60
61
    /** @var \Psr\Log\LoggerInterface */
62
    protected $logger;
63
64
    public function __construct(
65
        LocationService $locationService,
66
        URLAliasService $urlAliasService,
67
        ContentService $contentService,
68
        UrlAliasGenerator $generator,
69
        RequestContext $requestContext,
70
        LoggerInterface $logger = null
71
    ) {
72
        $this->locationService = $locationService;
73
        $this->urlAliasService = $urlAliasService;
74
        $this->contentService = $contentService;
75
        $this->generator = $generator;
76
        $this->requestContext = $requestContext !== null ? $requestContext : new RequestContext();
77
        $this->logger = $logger;
78
    }
79
80
    /**
81
     * Injects current root Location id.
82
     *
83
     * @param int|string $rootLocationId
84
     */
85
    public function setRootLocationId($rootLocationId)
86
    {
87
        $this->rootLocationId = $rootLocationId;
88
    }
89
90
    /**
91
     * Tries to match a request with a set of routes.
92
     *
93
     * If the matcher can not find information, it must throw one of the exceptions documented
94
     * below.
95
     *
96
     * @param Request $request The request to match
97
     *
98
     * @return array An array of parameters
99
     *
100
     * @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException If no matching resource could be found
101
     */
102
    public function matchRequest(Request $request)
103
    {
104
        try {
105
            $requestedPath = $request->attributes->get('semanticPathinfo', $request->getPathInfo());
106
            $urlAlias = $this->getUrlAlias($requestedPath);
107
            if ($this->rootLocationId === null) {
108
                $pathPrefix = '/';
109
            } else {
110
                $pathPrefix = $this->generator->getPathPrefixByRootLocationId($this->rootLocationId);
111
            }
112
113
            $params = [
114
                '_route' => self::URL_ALIAS_ROUTE_NAME,
115
            ];
116
            switch ($urlAlias->type) {
117
                case URLAlias::LOCATION:
118
                    $location = $this->generator->loadLocation($urlAlias->destination);
119
                    $params += [
120
                        '_controller' => static::VIEW_ACTION,
121
                        'contentId' => $location->contentId,
122
                        'locationId' => $urlAlias->destination,
123
                        'viewType' => ViewManager::VIEW_TYPE_FULL,
124
                        'layout' => true,
125
                    ];
126
127
                    // For Location alias setup 301 redirect to Location's current URL when:
128
                    // 1. alias is history
129
                    // 2. alias is custom with forward flag true
130
                    // 3. requested URL is not case-sensitive equal with the one loaded
131
                    if ($urlAlias->isHistory === true || ($urlAlias->isCustom === true && $urlAlias->forward === true)) {
132
                        $params += [
133
                            'semanticPathinfo' => $this->generator->generate($location, []),
134
                            'needsRedirect' => true,
135
                            // Specify not to prepend siteaccess while redirecting when applicable since it would be already present (see UrlAliasGenerator::doGenerate())
136
                            'prependSiteaccessOnRedirect' => false,
137
                        ];
138
                    } elseif ($this->needsCaseRedirect($urlAlias, $requestedPath, $pathPrefix)) {
139
                        $params += [
140
                            'semanticPathinfo' => $this->removePathPrefix($urlAlias->path, $pathPrefix),
141
                            'needsRedirect' => true,
142
                        ];
143
144
                        if ($urlAlias->destination instanceof Location) {
145
                            $params += ['locationId' => $urlAlias->destination->id];
146
                        }
147
                    }
148
149
                    if (isset($this->logger)) {
150
                        $this->logger->info("UrlAlias matched location #{$urlAlias->destination}. Forwarding to ViewController");
151
                    }
152
153
                    break;
154
155
                case URLAlias::RESOURCE:
156
                    // In URLAlias terms, "forward" means "redirect".
157
                    if ($urlAlias->forward) {
158
                        $params += [
159
                            'semanticPathinfo' => '/' . trim($urlAlias->destination, '/'),
160
                            'needsRedirect' => true,
161
                        ];
162
                    } elseif ($this->needsCaseRedirect($urlAlias, $requestedPath, $pathPrefix)) {
163
                        // Handle case-correction redirect
164
                        $params += [
165
                            'semanticPathinfo' => $this->removePathPrefix($urlAlias->path, $pathPrefix),
166
                            'needsRedirect' => true,
167
                        ];
168
                    } else {
169
                        $params += [
170
                            'semanticPathinfo' => '/' . trim($urlAlias->destination, '/'),
171
                            'needsForward' => true,
172
                        ];
173
                    }
174
175
                    break;
176
177
                case URLAlias::VIRTUAL:
178
                    // Handle case-correction redirect
179
                    if ($this->needsCaseRedirect($urlAlias, $requestedPath, $pathPrefix)) {
180
                        $params += [
181
                            'semanticPathinfo' => $this->removePathPrefix($urlAlias->path, $pathPrefix),
182
                            'needsRedirect' => true,
183
                        ];
184
                    } else {
185
                        // Virtual aliases should load the Content at homepage URL
186
                        $params += [
187
                            'semanticPathinfo' => '/',
188
                            'needsForward' => true,
189
                        ];
190
                    }
191
192
                    break;
193
            }
194
195
            return $params;
196
        } catch (NotFoundException $e) {
197
            throw new ResourceNotFoundException($e->getMessage(), $e->getCode(), $e);
198
        }
199
    }
200
201
    /**
202
     * Removes prefix from path.
203
     *
204
     * Checks for presence of $prefix and removes it from $path if found.
205
     *
206
     * @param string $path
207
     * @param string $prefix
208
     *
209
     * @return string
210
     */
211
    protected function removePathPrefix($path, $prefix)
212
    {
213
        if ($prefix !== '/' && mb_stripos($path, $prefix) === 0) {
214
            $path = mb_substr($path, mb_strlen($prefix));
215
        }
216
217
        return $path;
218
    }
219
220
    /**
221
     * Returns true of false on comparing $urlAlias->path and $path with case sensitivity.
222
     *
223
     * Used to determine if redirect is needed because requested path is case-different
224
     * from the stored one.
225
     *
226
     * @param \eZ\Publish\API\Repository\Values\Content\URLAlias $loadedUrlAlias
227
     * @param string $requestedPath
228
     * @param string $pathPrefix
229
     *
230
     * @return bool
231
     */
232
    protected function needsCaseRedirect(URLAlias $loadedUrlAlias, $requestedPath, $pathPrefix)
233
    {
234
        // If requested path is excluded from tree root jail, compare it to loaded UrlAlias directly.
235
        if ($this->generator->isUriPrefixExcluded($requestedPath)) {
236
            return strcmp($loadedUrlAlias->path, $requestedPath) !== 0;
237
        }
238
239
        // Compare loaded UrlAlias with requested path, prefixed with configured path prefix.
240
        return
241
            strcmp(
242
                $loadedUrlAlias->path,
243
                $pathPrefix . ($pathPrefix === '/' ? trim($requestedPath, '/') : rtrim($requestedPath, '/'))
244
            ) !== 0
245
        ;
246
    }
247
248
    /**
249
     * Returns the UrlAlias object to use, starting from the request.
250
     *
251
     * @param $pathinfo
252
     *
253
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the path does not exist or is not valid for the given language
254
     *
255
     * @return URLAlias
256
     */
257
    protected function getUrlAlias($pathinfo)
258
    {
259
        return $this->urlAliasService->lookup($pathinfo);
260
    }
261
262
    /**
263
     * Gets the RouteCollection instance associated with this Router.
264
     *
265
     * @return RouteCollection A RouteCollection instance
266
     */
267
    public function getRouteCollection()
268
    {
269
        return new RouteCollection();
270
    }
271
272
    /**
273
     * Generates a URL for a location, from the given parameters.
274
     *
275
     * It is possible to directly pass a Location object as the route name, as the ChainRouter allows it through ChainedRouterInterface.
276
     *
277
     * If $name is a route name, the "location" key in $parameters must be set to a valid eZ\Publish\API\Repository\Values\Content\Location object.
278
     * "locationId" can also be provided.
279
     *
280
     * If the generator is not able to generate the url, it must throw the RouteNotFoundException
281
     * as documented below.
282
     *
283
     * @see UrlAliasRouter::supports()
284
     *
285
     * @param string $name The name of the route or a Location instance
286
     * @param array $parameters An array of parameters
287
     * @param int $referenceType The type of reference to be generated (one of the constants)
288
     *
289
     * @throws \LogicException
290
     * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
291
     * @throws \InvalidArgumentException
292
     *
293
     * @return string The generated URL
294
     *
295
     * @api
296
     */
297
    public function generate(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
298
    {
299
        if ($name === '' &&
300
            array_key_exists(RouteObjectInterface::ROUTE_OBJECT, $parameters) &&
301
            $this->supportsObject($parameters[RouteObjectInterface::ROUTE_OBJECT])
302
        ) {
303
            $location = $parameters[RouteObjectInterface::ROUTE_OBJECT];
304
            unset($parameters[RouteObjectInterface::ROUTE_OBJECT]);
305
306
            return $this->generator->generate($location, $parameters, $referenceType);
307
        }
308
309
        // Normal route name
310
        if ($name === self::URL_ALIAS_ROUTE_NAME) {
311
            if (isset($parameters['location']) || isset($parameters['locationId'])) {
312
                // Check if location is a valid Location object
313
                if (isset($parameters['location']) && !$parameters['location'] instanceof Location) {
314
                    throw new LogicException(
315
                        "When generating a UrlAlias route, the 'location' parameter must be a valid " . Location::class . '.'
316
                    );
317
                }
318
319
                $location = isset($parameters['location']) ? $parameters['location'] : $this->locationService->loadLocation($parameters['locationId']);
320
                unset($parameters['location'], $parameters['locationId'], $parameters['viewType'], $parameters['layout']);
321
322
                return $this->generator->generate($location, $parameters, $referenceType);
323
            }
324
325
            if (isset($parameters['contentId'])) {
326
                $contentInfo = $this->contentService->loadContentInfo($parameters['contentId']);
327
                unset($parameters['contentId'], $parameters['viewType'], $parameters['layout']);
328
329
                if (empty($contentInfo->mainLocationId)) {
330
                    throw new LogicException('Cannot generate a UrlAlias route for content without main Location.');
331
                }
332
333
                return $this->generator->generate(
334
                    $this->locationService->loadLocation($contentInfo->mainLocationId),
335
                    $parameters,
336
                    $referenceType
337
                );
338
            }
339
340
            throw new InvalidArgumentException(
341
                "When generating a UrlAlias route, either 'location', 'locationId', or 'contentId' must be provided."
342
            );
343
        }
344
345
        throw new RouteNotFoundException('Could not match route');
346
    }
347
348
    public function setContext(RequestContext $context)
349
    {
350
        $this->requestContext = $context;
351
        $this->generator->setRequestContext($context);
352
    }
353
354
    public function getContext()
355
    {
356
        return $this->requestContext;
357
    }
358
359
    /**
360
     * Not supported. Please use matchRequest() instead.
361
     *
362
     * @param $pathinfo
363
     *
364
     * @throws \RuntimeException
365
     */
366
    public function match($pathinfo)
367
    {
368
        throw new \RuntimeException("The UrlAliasRouter doesn't support the match() method. Use matchRequest() instead.");
369
    }
370
371
    /**
372
     * Whether the router supports the thing in $name to generate a route.
373
     *
374
     * This check does not need to look if the specific instance can be
375
     * resolved to a route, only whether the router can generate routes from
376
     * objects of this class.
377
     *
378
     * @param mixed $name The route name or route object
379
     *
380
     * @return bool
381
     */
382
    public function supports($name)
383
    {
384
        return $name === self::URL_ALIAS_ROUTE_NAME || $this->supportsObject($name);
385
    }
386
387
    private function supportsObject($object): bool
388
    {
389
        return $object instanceof Location;
390
    }
391
392
    /**
393
     * @see \Symfony\Cmf\Component\Routing\VersatileGeneratorInterface::getRouteDebugMessage()
394
     */
395
    public function getRouteDebugMessage($name, array $parameters = [])
396
    {
397
        if ($name instanceof RouteObjectInterface) {
398
            return 'Route with key ' . $name->getRouteKey();
399
        }
400
401
        if ($name instanceof SymfonyRoute) {
402
            return 'Route with pattern ' . $name->getPath();
403
        }
404
405
        return $name;
406
    }
407
}
408