Completed
Push — EZP-31000 ( 2d6acd )
by
unknown
20:00
created

UrlAliasGenerator   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 0
loc 240
rs 10
c 0
b 0
f 0
wmc 28
lcom 1
cbo 9

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A doGenerate() 0 12 1
A setRootLocationId() 0 4 1
A setExcludedUriPrefixes() 0 4 1
A getPathPrefixByRootLocationId() 0 24 4
A isUriPrefixExcluded() 0 11 3
A loadLocation() 0 9 1
B createPathString() 0 39 11
A createQueryString() 0 19 4
A filterCharactersOfURL() 0 4 1
1
<?php
2
3
/**
4
 * File containing the UrlAliasGenerator 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\Generator;
10
11
use eZ\Publish\API\Repository\Repository;
12
use eZ\Publish\API\Repository\Values\Content\Location;
13
use eZ\Publish\Core\MVC\ConfigResolverInterface;
14
use eZ\Publish\Core\MVC\Symfony\Routing\Generator;
15
use Symfony\Component\Routing\RouterInterface;
16
17
/**
18
 * URL generator for UrlAlias based links.
19
 *
20
 * @see \eZ\Publish\Core\MVC\Symfony\Routing\UrlAliasRouter
21
 */
22
class UrlAliasGenerator extends Generator
23
{
24
    const INTERNAL_LOCATION_ROUTE = '_ezpublishLocation';
25
    const INTERNAL_CONTENT_VIEW_ROUTE = '_ez_content_view';
26
27
    /** @var \eZ\Publish\Core\Repository\Repository */
28
    private $repository;
29
30
    /**
31
     * The default router (that works with declared routes).
32
     *
33
     * @var \Symfony\Component\Routing\RouterInterface
34
     */
35
    private $defaultRouter;
36
37
    /** @var int */
38
    private $rootLocationId;
39
40
    /** @var array */
41
    private $excludedUriPrefixes = [];
42
43
    /** @var array */
44
    private $pathPrefixMap = [];
45
46
    /** @var \eZ\Publish\Core\MVC\ConfigResolverInterface */
47
    private $configResolver;
48
49
    /**
50
     * Array of characters that are potentially unsafe for output for (x)html, json, etc,
51
     * and respective url-encoded value.
52
     *
53
     * @var array
54
     */
55
    private $unsafeCharMap;
56
57
    public function __construct(Repository $repository, RouterInterface $defaultRouter, ConfigResolverInterface $configResolver, array $unsafeCharMap = [])
58
    {
59
        $this->repository = $repository;
0 ignored issues
show
Documentation Bug introduced by
$repository is of type object<eZ\Publish\API\Repository\Repository>, but the property $repository was declared to be of type object<eZ\Publish\Core\Repository\Repository>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
60
        $this->defaultRouter = $defaultRouter;
61
        $this->configResolver = $configResolver;
62
        $this->unsafeCharMap = $unsafeCharMap;
63
    }
64
65
    /**
66
     * Generates the URL from $urlResource and $parameters.
67
     * Entries in $parameters will be added in the query string.
68
     *
69
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
70
     * @param array $parameters
71
     *
72
     * @return string
73
     */
74
    public function doGenerate($location, array $parameters)
75
    {
76
        $siteaccess = $parameters['siteaccess'] ?? null;
77
78
        unset($parameters['language'], $parameters['contentId'], $parameters['siteaccess']);
79
80
        $pathString = $this->createPathString($location, $siteaccess);
81
        $queryString = $this->createQueryString($parameters);
82
        $url = $pathString . $queryString;
83
84
        return $this->filterCharactersOfURL($url);
85
    }
86
87
    /**
88
     * Injects current root locationId that will be used for link generation.
89
     *
90
     * @param int $rootLocationId
91
     */
92
    public function setRootLocationId($rootLocationId)
93
    {
94
        $this->rootLocationId = $rootLocationId;
95
    }
96
97
    /**
98
     * @param array $excludedUriPrefixes
99
     */
100
    public function setExcludedUriPrefixes(array $excludedUriPrefixes)
101
    {
102
        $this->excludedUriPrefixes = $excludedUriPrefixes;
103
    }
104
105
    /**
106
     * Returns path corresponding to $rootLocationId.
107
     *
108
     * @param int $rootLocationId
109
     * @param array $languages
110
     * @param string $siteaccess
111
     *
112
     * @return string
113
     */
114
    public function getPathPrefixByRootLocationId($rootLocationId, $languages = null, $siteaccess = null)
115
    {
116
        if (!$rootLocationId) {
117
            return '';
118
        }
119
120
        if (!isset($this->pathPrefixMap[$siteaccess])) {
121
            $this->pathPrefixMap[$siteaccess] = [];
122
        }
123
124
        if (!isset($this->pathPrefixMap[$siteaccess][$rootLocationId])) {
125
            $this->pathPrefixMap[$siteaccess][$rootLocationId] = $this->repository
126
                ->getURLAliasService()
127
                ->reverseLookup(
128
                    $this->loadLocation($rootLocationId),
129
                    null,
130
                    false,
131
                    $languages
132
                )
133
                ->path;
134
        }
135
136
        return $this->pathPrefixMap[$siteaccess][$rootLocationId];
137
    }
138
139
    /**
140
     * Checks if passed URI has an excluded prefix, when a root location is defined.
141
     *
142
     * @param string $uri
143
     *
144
     * @return bool
145
     */
146
    public function isUriPrefixExcluded($uri)
147
    {
148
        foreach ($this->excludedUriPrefixes as $excludedPrefix) {
149
            $excludedPrefix = '/' . trim($excludedPrefix, '/');
150
            if (mb_stripos($uri, $excludedPrefix) === 0) {
151
                return true;
152
            }
153
        }
154
155
        return false;
156
    }
157
158
    /**
159
     * Loads a location by its locationId, regardless to user limitations since the router is invoked BEFORE security (no user authenticated yet).
160
     * Not to be used for link generation.
161
     *
162
     * @param int $locationId
163
     *
164
     * @return \eZ\Publish\Core\Repository\Values\Content\Location
165
     */
166
    public function loadLocation($locationId)
167
    {
168
        return $this->repository->sudo(
169
            function (Repository $repository) use ($locationId) {
170
                /* @var $repository \eZ\Publish\Core\Repository\Repository */
171
                return $repository->getLocationService()->loadLocation($locationId);
172
            }
173
        );
174
    }
175
176
    /**
177
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
178
     * @param string|null $siteaccess
179
     *
180
     * @return string
181
     */
182
    private function createPathString(Location $location, ?string $siteaccess = null): string
183
    {
184
        $urlAliasService = $this->repository->getURLAliasService();
185
186
        if ($siteaccess) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $siteaccess of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
187
            // We generate for a different SiteAccess, so potentially in a different language.
188
            $languages = $this->configResolver->getParameter('languages', null, $siteaccess);
189
            $urlAliases = $urlAliasService->listLocationAliases($location, false, null, null, $languages);
190
            // Use the target SiteAccess root location
191
            $rootLocationId = $this->configResolver->getParameter('content.tree_root.location_id', null, $siteaccess);
192
        } else {
193
            $languages = null;
194
            $urlAliases = $urlAliasService->listLocationAliases($location, false);
195
            $rootLocationId = $this->rootLocationId;
196
        }
197
198
        if (!empty($urlAliases)) {
199
            $path = $urlAliases[0]->path;
200
            // Remove rootLocation's prefix if needed.
201
            if ($rootLocationId !== null) {
202
                $pathPrefix = $this->getPathPrefixByRootLocationId($rootLocationId, $languages, $siteaccess);
203
                // "/" cannot be considered as a path prefix since it's root, so we ignore it.
204
                if ($pathPrefix !== '/' && ($path === $pathPrefix || mb_stripos($path, $pathPrefix . '/') === 0)) {
205
                    $path = mb_substr($path, mb_strlen($pathPrefix));
206
                } elseif ($pathPrefix !== '/' && !$this->isUriPrefixExcluded($path) && $this->logger !== null) {
207
                    // Location path is outside configured content tree and doesn't have an excluded prefix.
208
                    // This is most likely an error (from content edition or link generation logic).
209
                    $this->logger->warning("Generating a link to a location outside root content tree: '$path' is outside tree starting to location #$rootLocationId");
210
                }
211
            }
212
        } else {
213
            $path = $this->defaultRouter->generate(
214
                self::INTERNAL_CONTENT_VIEW_ROUTE,
215
                ['contentId' => $location->contentId, 'locationId' => $location->id]
216
            );
217
        }
218
219
        return $path ?: '/';
220
    }
221
222
    /**
223
     * Creates query string from parameters. If `_fragment` parameter is provided then
224
     * fragment identifier is added at the end of the URL.
225
     *
226
     * @param array $parameters
227
     *
228
     * @return string
229
     */
230
    private function createQueryString(array $parameters): string
231
    {
232
        $queryString = '';
233
        $fragment = null;
234
        if (isset($parameters['_fragment'])) {
235
            $fragment = $parameters['_fragment'];
236
            unset($parameters['_fragment']);
237
        }
238
239
        if (!empty($parameters)) {
240
            $queryString = '?' . http_build_query($parameters, '', '&');
241
        }
242
243
        if ($fragment) {
244
            $queryString .= '#' . strtr(rawurlencode($fragment), ['%2F' => '/', '%3F' => '?']);
245
        }
246
247
        return $queryString;
248
    }
249
250
    /**
251
     * Replace potentially unsafe characters with url-encoded counterpart.
252
     *
253
     * @param string $url
254
     *
255
     * @return string
256
     */
257
    private function filterCharactersOfURL(string $url): string
258
    {
259
        return strtr($url, $this->unsafeCharMap);
260
    }
261
}
262