|
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)) { |
|
|
|
|
|
|
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)) { |
|
|
|
|
|
|
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
|
|
|
|
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.