Completed
Push — fallback_language_fields ( 531860 )
by André
37:34
created

UrlAliasRouter::getRouteCollection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 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
 * @version //autogentag//
10
 */
11
namespace eZ\Publish\Core\MVC\Symfony\Routing;
12
13
use eZ\Publish\API\Repository\LocationService;
14
use eZ\Publish\API\Repository\URLAliasService;
15
use eZ\Publish\API\Repository\ContentService;
16
use eZ\Publish\API\Repository\Values\Content\URLAlias;
17
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
18
use eZ\Publish\API\Repository\Values\Content\Location;
19
use eZ\Publish\Core\MVC\Symfony\View\Manager as ViewManager;
20
use eZ\Publish\Core\MVC\Symfony\Routing\Generator\UrlAliasGenerator;
21
use Symfony\Cmf\Component\Routing\ChainedRouterInterface;
22
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
23
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\Routing\RequestContext;
26
use Psr\Log\LoggerInterface;
27
use Symfony\Component\Routing\RouteCollection;
28
use Symfony\Component\Routing\Route as SymfonyRoute;
29
use Symfony\Component\Routing\Exception\RouteNotFoundException;
30
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
31
use InvalidArgumentException;
32
use LogicException;
33
34
class UrlAliasRouter implements ChainedRouterInterface, RequestMatcherInterface
35
{
36
    const URL_ALIAS_ROUTE_NAME = 'ez_urlalias';
37
38
    /**
39
     * @deprecated since 6.0.0.
40
     */
41
    const LOCATION_VIEW_CONTROLLER = 'ez_content:viewLocation';
42
43
    /**
44
     * @since 6.0.0
45
     */
46
    const VIEW_ACTION = 'ez_content:viewAction';
47
48
    /**
49
     * @var \Symfony\Component\Routing\RequestContext
50
     */
51
    protected $requestContext;
52
53
    /**
54
     * @var \eZ\Publish\API\Repository\LocationService
55
     */
56
    protected $locationService;
57
58
    /**
59
     * @var \eZ\Publish\API\Repository\URLAliasService
60
     */
61
    protected $urlAliasService;
62
63
    /**
64
     * @var \eZ\Publish\API\Repository\ContentService
65
     */
66
    protected $contentService;
67
68
    /**
69
     * @var \eZ\Publish\Core\MVC\Symfony\Routing\Generator\UrlAliasGenerator
70
     */
71
    protected $generator;
72
73
    /**
74
     * Holds current root Location id.
75
     *
76
     * @var int|string
77
     */
78
    protected $rootLocationId;
79
80
    /**
81
     * @var \Psr\Log\LoggerInterface
82
     */
83
    protected $logger;
84
85
    public function __construct(
86
        LocationService $locationService,
87
        URLAliasService $urlAliasService,
88
        ContentService $contentService,
89
        UrlAliasGenerator $generator,
90
        RequestContext $requestContext,
91
        LoggerInterface $logger = null
92
    ) {
93
        $this->locationService = $locationService;
94
        $this->urlAliasService = $urlAliasService;
95
        $this->contentService = $contentService;
96
        $this->generator = $generator;
97
        $this->requestContext = $requestContext !== null ? $requestContext : new RequestContext();
98
        $this->logger = $logger;
99
    }
100
101
    /**
102
     * Injects current root Location id.
103
     *
104
     * @param int|string $rootLocationId
105
     */
106
    public function setRootLocationId($rootLocationId)
107
    {
108
        $this->rootLocationId = $rootLocationId;
109
    }
110
111
    /**
112
     * Tries to match a request with a set of routes.
113
     *
114
     * If the matcher can not find information, it must throw one of the exceptions documented
115
     * below.
116
     *
117
     * @param Request $request The request to match
118
     *
119
     * @return array An array of parameters
120
     *
121
     * @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException If no matching resource could be found
122
     */
123
    public function matchRequest(Request $request)
124
    {
125
        try {
126
            $requestedPath = rawurldecode($request->attributes->get('semanticPathinfo', $request->getPathInfo()));
127
            $urlAlias = $this->getUrlAlias($requestedPath);
128
            if ($this->rootLocationId === null) {
129
                $pathPrefix = '/';
130
            } else {
131
                $pathPrefix = $this->generator->getPathPrefixByRootLocationId($this->rootLocationId);
132
            }
133
134
            $params = array(
135
                '_route' => self::URL_ALIAS_ROUTE_NAME,
136
            );
137
            switch ($urlAlias->type) {
138
                case URLAlias::LOCATION:
139
                    $location = $this->generator->loadLocation($urlAlias->destination);
140
                    $params += array(
141
                        '_controller' => static::VIEW_ACTION,
142
                        'contentId' => $location->contentId,
143
                        'locationId' => $urlAlias->destination,
144
                        'viewType' => ViewManager::VIEW_TYPE_FULL,
145
                        'layout' => true,
146
                    );
147
148
                    $request->attributes->set('locationId', $urlAlias->destination);
149
150
                    // For Location alias setup 301 redirect to Location's current URL when:
151
                    // 1. alias is history
152
                    // 2. alias is custom with forward flag true
153
                    // 3. requested URL is not case-sensitive equal with the one loaded
154
                    if ($urlAlias->isHistory === true || ($urlAlias->isCustom === true && $urlAlias->forward === true)) {
155
                        $request->attributes->set('semanticPathinfo', $this->generate($location));
156
                        $request->attributes->set('needsRedirect', true);
157
                        // Specify not to prepend siteaccess while redirecting when applicable since it would be already present (see UrlAliasGenerator::doGenerate())
158
                        $request->attributes->set('prependSiteaccessOnRedirect', false);
159 View Code Duplication
                    } elseif ($this->needsCaseRedirect($urlAlias, $requestedPath, $pathPrefix)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
160
                        $request->attributes->set('semanticPathinfo', $this->removePathPrefix($urlAlias->path, $pathPrefix));
161
                        $request->attributes->set('needsRedirect', true);
162
                    }
163
164
                    if (isset($this->logger)) {
165
                        $this->logger->info("UrlAlias matched location #{$urlAlias->destination}. Forwarding to ViewController");
166
                    }
167
168
                    break;
169
170
                case URLAlias::RESOURCE:
171
                    // In URLAlias terms, "forward" means "redirect".
172
                    if ($urlAlias->forward) {
173
                        $request->attributes->set('semanticPathinfo', '/' . trim($urlAlias->destination, '/'));
174
                        $request->attributes->set('needsRedirect', true);
175 View Code Duplication
                    } elseif ($this->needsCaseRedirect($urlAlias, $requestedPath, $pathPrefix)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
176
                        // Handle case-correction redirect
177
                        $request->attributes->set('semanticPathinfo', $this->removePathPrefix($urlAlias->path, $pathPrefix));
178
                        $request->attributes->set('needsRedirect', true);
179
                    } else {
180
                        $request->attributes->set('semanticPathinfo', '/' . trim($urlAlias->destination, '/'));
181
                        $request->attributes->set('needsForward', true);
182
                    }
183
184
                    break;
185
186
                case URLAlias::VIRTUAL:
187
                    // Handle case-correction redirect
188
                    if ($this->needsCaseRedirect($urlAlias, $requestedPath, $pathPrefix)) {
189
                        $request->attributes->set('semanticPathinfo', $this->removePathPrefix($urlAlias->path, $pathPrefix));
190
                        $request->attributes->set('needsRedirect', true);
191
                    } else {
192
                        // Virtual aliases should load the Content at homepage URL
193
                        $request->attributes->set('semanticPathinfo', '/');
194
                        $request->attributes->set('needsForward', true);
195
                    }
196
197
                    break;
198
            }
199
200
            return $params;
201
        } catch (NotFoundException $e) {
202
            throw new ResourceNotFoundException($e->getMessage(), $e->getCode(), $e);
203
        }
204
    }
205
206
    /**
207
     * Removes prefix from path.
208
     *
209
     * Checks for presence of $prefix and removes it from $path if found.
210
     *
211
     * @param string $path
212
     * @param string $prefix
213
     *
214
     * @return string
215
     */
216
    protected function removePathPrefix($path, $prefix)
217
    {
218
        if ($prefix !== '/' && mb_stripos($path, $prefix) === 0) {
219
            $path = mb_substr($path, mb_strlen($prefix));
220
        }
221
222
        return $path;
223
    }
224
225
    /**
226
     * Returns true of false on comparing $urlAlias->path and $path with case sensitivity.
227
     *
228
     * Used to determine if redirect is needed because requested path is case-different
229
     * from the stored one.
230
     *
231
     * @param \eZ\Publish\API\Repository\Values\Content\URLAlias $loadedUrlAlias
232
     * @param string $requestedPath
233
     * @param string $pathPrefix
234
     *
235
     * @return bool
236
     */
237
    protected function needsCaseRedirect(URLAlias $loadedUrlAlias, $requestedPath, $pathPrefix)
238
    {
239
        // If requested path is excluded from tree root jail, compare it to loaded UrlAlias directly.
240
        if ($this->generator->isUriPrefixExcluded($requestedPath)) {
241
            return strcmp($loadedUrlAlias->path, $requestedPath) !== 0;
242
        }
243
244
        // Compare loaded UrlAlias with requested path, prefixed with configured path prefix.
245
        return (
246
            strcmp(
247
                $loadedUrlAlias->path,
248
                $pathPrefix . ($pathPrefix === '/' ? trim($requestedPath, '/') : rtrim($requestedPath, '/'))
249
            ) !== 0
250
        );
251
    }
252
253
    /**
254
     * Returns the UrlAlias object to use, starting from the request.
255
     *
256
     * @param $pathinfo
257
     *
258
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the path does not exist or is not valid for the given language
259
     *
260
     * @return URLAlias
261
     */
262
    protected function getUrlAlias($pathinfo)
263
    {
264
        return $this->urlAliasService->lookup($pathinfo);
265
    }
266
267
    /**
268
     * Gets the RouteCollection instance associated with this Router.
269
     *
270
     * @return RouteCollection A RouteCollection instance
271
     */
272
    public function getRouteCollection()
273
    {
274
        return new RouteCollection();
275
    }
276
277
    /**
278
     * Generates a URL for a location, from the given parameters.
279
     *
280
     * It is possible to directly pass a Location object as the route name, as the ChainRouter allows it through ChainedRouterInterface.
281
     *
282
     * 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.
283
     * "locationId" can also be provided.
284
     *
285
     * If the generator is not able to generate the url, it must throw the RouteNotFoundException
286
     * as documented below.
287
     *
288
     * @see UrlAliasRouter::supports()
289
     *
290
     * @param string|\eZ\Publish\API\Repository\Values\Content\Location $name The name of the route or a Location instance
291
     * @param mixed $parameters An array of parameters
292
     * @param bool $absolute Whether to generate an absolute URL
293
     *
294
     * @throws \LogicException
295
     * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
296
     * @throws \InvalidArgumentException
297
     *
298
     * @return string The generated URL
299
     *
300
     * @api
301
     */
302
    public function generate($name, $parameters = array(), $absolute = false)
303
    {
304
        // Direct access to Location
305
        if ($name instanceof Location) {
306
            return $this->generator->generate($name, $parameters, $absolute);
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 an UrlAlias route, 'location' parameter must be a valid eZ\\Publish\\API\\Repository\\Values\\Content\\Location."
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, $absolute);
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 an UrlAlias route for content without main location.');
331
                }
332
333
                return $this->generator->generate(
334
                    $this->locationService->loadLocation($contentInfo->mainLocationId),
335
                    $parameters,
336
                    $absolute
337
                );
338
            }
339
340
            throw new InvalidArgumentException(
341
                "When generating an 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. Please 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 instanceof Location || $name === self::URL_ALIAS_ROUTE_NAME;
385
    }
386
387
    /**
388
     * @see Symfony\Cmf\Component\Routing\VersatileGeneratorInterface::getRouteDebugMessage()
389
     */
390
    public function getRouteDebugMessage($name, array $parameters = array())
391
    {
392
        if ($name instanceof RouteObjectInterface) {
393
            return 'Route with key ' . $name->getRouteKey();
394
        }
395
396
        if ($name instanceof SymfonyRoute) {
397
            return 'Route with pattern ' . $name->getPath();
398
        }
399
400
        return $name;
401
    }
402
}
403