Listener   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Test Coverage

Coverage 63.64%

Importance

Changes 17
Bugs 3 Features 2
Metric Value
eloc 105
c 17
b 3
f 2
dl 0
loc 257
ccs 70
cts 110
cp 0.6364
rs 9.68
wmc 34

8 Methods

Rating   Name   Duplication   Size   Complexity  
A setIsParamsEnabled() 0 3 1
A setExcludePatterns() 0 3 1
A isExcluded() 0 10 3
A rewriteResponse() 0 19 4
A rewriteRequest() 0 23 1
A __construct() 0 4 1
C onKernelRequest() 0 66 14
B onKernelResponse() 0 50 9
1
<?php
2
/**
3
 * @author Gerard van Helden <[email protected]>
4
 * @copyright Zicht Online <http://zicht.nl>
5
 */
6
7
namespace Zicht\Bundle\UrlBundle\Aliasing;
8
9
use Symfony\Component\HttpFoundation\Request;
10
use Symfony\Component\HttpFoundation\Response;
11
use Symfony\Component\HttpKernel\Event;
12
use Symfony\Component\HttpFoundation\RedirectResponse;
13
use Symfony\Component\HttpKernel\EventListener\RouterListener;
14
use Symfony\Component\HttpKernel\HttpKernelInterface;
15
use Zicht\Bundle\UrlBundle\Aliasing\Mapper\UrlMapperInterface;
16
use Zicht\Bundle\UrlBundle\Url\Params\UriParser;
17
use Zicht\Bundle\UrlBundle\Entity\UrlAlias;
18
19
/**
20
 * Listens to incoming and outgoing requests to handle url aliasing at the kernel master request level.
21
 */
22
class Listener
23
{
24
    protected $aliasing;
25
26
    protected $excludePatterns = array();
27
    protected $isParamsEnabled = false;
28
29
    /**
30
     * Construct the aliasing listener.
31
     *
32
     * @param Aliasing $aliasing
33
     * @param RouterListener $router
34
     */
35 11
    public function __construct(Aliasing $aliasing, RouterListener $router)
36
    {
37 11
        $this->aliasing = $aliasing;
38 11
        $this->router = $router;
0 ignored issues
show
Bug Best Practice introduced by
The property router does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
39 11
    }
40
41
    /**
42
     * Listens to redirect responses, to replace any internal url with a public one.
43
     *
44
     * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $e
45
     * @return void
46
     */
47
    public function onKernelResponse(Event\FilterResponseEvent $e)
48
    {
49
        if ($e->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
50
            $response = $e->getResponse();
51
52
            // only do anything if the response has a Location header
53
            if (false !== ($location = $response->headers->get('location', false))) {
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type null|string|string[] expected by parameter $default of Symfony\Component\HttpFoundation\HeaderBag::get(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

53
            if (false !== ($location = $response->headers->get('location', /** @scrutinizer ignore-type */ false))) {
Loading history...
introduced by
The condition false !== $location = $r...>get('location', false) is always true.
Loading history...
54
                $absolutePrefix = $e->getRequest()->getSchemeAndHttpHost();
55
56
                if (parse_url($location, PHP_URL_SCHEME)) {
57
                    if (substr($location, 0, strlen($absolutePrefix)) === $absolutePrefix) {
58
                        $relative = substr($location, strlen($absolutePrefix));
59
                    } else {
60
                        $relative = null;
61
                    }
62
                } else {
63
                    $relative = $location;
64
                }
65
66
                // Possible suffix for the rewrite URL
67
                $suffix = '';
68
69
                /**
70
                 * Catches the following situation:
71
                 *
72
                 * Some redirect URLs might contain extra URL parameters in the form of:
73
                 *
74
                 *      /nl/page/666/terms=FOO/tag=BAR
75
                 *
76
                 * (E.g. some SOLR implementations use this URL scheme)
77
                 *
78
                 * The relative URL above is then incorrect and the public alias can not be found.
79
                 *
80
                 * Remove /terms=FOO/tag=BAR from the relative path and attach to clean URL if found.
81
                 *
82
                 */
83
                if (preg_match('/^(\/[a-z]{2,2}\/page\/\d+)(.*)$/', $relative, $matches)) {
84
                    list(, $relative, $suffix) = $matches;
85
                } elseif (preg_match('/^(\/page\/\d+)(.*)$/', $relative, $matches)) {
86
                    /* For old sites that don't have the locale in the URI */
87
                    list(, $relative, $suffix) = $matches;
88
                }
89
90
                if (null !== $relative && null !== ($url = $this->aliasing->hasPublicAlias($relative))) {
91
                    $rewrite = $absolutePrefix . $url . $suffix;
92
                    $response->headers->set('location', $rewrite);
93
                }
94
            }
95
96
            $this->rewriteResponse($e->getRequest(), $response);
97
        }
98
    }
99
100
    /**
101
     * Exclude patterns from aliasing
102
     *
103
     * @param array $excludePatterns
104
     * @return void
105
     */
106 2
    public function setExcludePatterns($excludePatterns)
107
    {
108 2
        $this->excludePatterns = $excludePatterns;
109 2
    }
110
111
    /**
112
     * Whether or not to consider URL parameters (key/value pairs at the end of the URL)
113
     *
114
     * @param bool $isParamsEnabled
115
     * @return void
116
     */
117 3
    public function setIsParamsEnabled($isParamsEnabled)
118
    {
119 3
        $this->isParamsEnabled = $isParamsEnabled;
120 3
    }
121
122
    /**
123
     * Returns true if the URL matches any of the exclude patterns
124
     *
125
     * @param string $url
126
     * @return bool
127
     */
128 10
    protected function isExcluded($url)
129
    {
130 10
        $ret = false;
131 10
        foreach ($this->excludePatterns as $pattern) {
132 2
            if (preg_match($pattern, $url)) {
133 1
                $ret = true;
134 2
                break;
135
            }
136
        }
137 10
        return $ret;
138
    }
139
140
141
    /**
142
     * Listens to master requests and translates the URL to an internal url, if there is an alias available
143
     *
144
     * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
145
     * @return void
146
     * @throws \UnexpectedValueException
147
     */
148 11
    public function onKernelRequest(Event\GetResponseEvent $event)
149
    {
150 11
        if ($event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
151 10
            $request = $event->getRequest();
152 10
            $publicUrl = rawurldecode($request->getRequestUri());
153
154 10
            if ($this->isExcluded($publicUrl)) {
155
                // don't process urls which are marked as excluded.
156 1
                return;
157
            }
158
159 9
            if ($this->isParamsEnabled) {
160 3
                if (false !== ($queryMark = strpos($publicUrl, '?'))) {
161
                    $originalUrl = $publicUrl;
162
                    $publicUrl = substr($originalUrl, 0, $queryMark);
163
                    $queryString = substr($originalUrl, $queryMark);
164
                } else {
165 3
                    $queryString = null;
166
                }
167
168 3
                $parts = explode('/', $publicUrl);
169 3
                $params = array();
170 3
                while (false !== strpos(end($parts), '=')) {
171 2
                    array_push($params, array_pop($parts));
172
                }
173 3
                if ($params) {
174 2
                    $publicUrl = join('/', $parts);
175
176 2
                    $parser = new UriParser();
177 2
                    $request->query->add($parser->parseUri(join('/', array_reverse($params))));
178
179 2
                    if (!$this->aliasing->hasInternalAlias($publicUrl, false)) {
180 1
                        $this->rewriteRequest($event, $publicUrl . $queryString);
181
182 1
                        return;
183
                    }
184
                }
185
            }
186
187
            /** @var UrlAlias $url */
188 8
            if ($url = $this->aliasing->hasInternalAlias($publicUrl, true)) {
189 6
                switch ($url->getMode()) {
190 6
                    case UrlAlias::REWRITE:
191 3
                        $this->rewriteRequest($event, $url->getInternalUrl());
192 3
                        break;
193 3
                    case UrlAlias::MOVE:
194 2
                    case UrlAlias::ALIAS:
195 2
                        $event->setResponse(new RedirectResponse($url->getInternalUrl(), $url->getMode()));
196 2
                        break;
197
                    default:
198 1
                        throw new \UnexpectedValueException(
199 1
                            sprintf(
200 1
                                "Invalid mode %s for UrlAlias %s.",
201 1
                                $url->getMode(),
202 6
                                json_encode($url)
203
                            )
204
                        );
205
                }
206 2
            } elseif (strpos($publicUrl, '?') !== false) {
207
                // allow aliases to receive the query string.
208
209
                $publicUrl = substr($publicUrl, 0, strpos($publicUrl, '?'));
210
                if ($url = $this->aliasing->hasInternalAlias($publicUrl, true, UrlAlias::REWRITE)) {
211
                    $this->rewriteRequest($event, $url->getInternalUrl());
212
213
                    return;
214
                }
215
            }
216
        }
217 8
    }
218
219
220
    /**
221
     * Route the request to the specified URL.
222
     *
223
     * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
224
     * @param string $url
225
     * @return void
226
     */
227 4
    public function rewriteRequest($event, $url)
228
    {
229
        // override the request's REQUEST_URI
230 4
        $event->getRequest()->initialize(
231 4
            $event->getRequest()->query->all(),
232 4
            $event->getRequest()->request->all(),
233 4
            $event->getRequest()->attributes->all(),
234 4
            $event->getRequest()->cookies->all(),
235 4
            $event->getRequest()->files->all(),
236
            array(
237 4
                'ORIGINAL_REQUEST_URI' => $event->getRequest()->server->get('REQUEST_URI'),
238 4
                'REQUEST_URI' => $url
239 4
            ) + $event->getRequest()->server->all(),
240 4
            $event->getRequest()->getContent()
241
        );
242
243
        // route the request
244 4
        $subEvent = new Event\GetResponseEvent(
245 4
            $event->getKernel(),
246 4
            $event->getRequest(),
247 4
            $event->getRequestType()
248
        );
249 4
        $this->router->onKernelRequest($subEvent);
250 4
    }
251
252
253
    /**
254
     * Rewrite URL's from internal naming to public aliases in the response.
255
     *
256
     * @param Request $request
257
     * @param Response $response
258
     * @return void
259
     */
260
    protected function rewriteResponse(Request $request, Response $response)
261
    {
262
        // for debugging purposes. Might need to be configurable.
263
        if ($request->query->get('__disable_aliasing')) {
264
            return;
265
        }
266
        if (preg_match('!^/admin/!', $request->getRequestUri())) {
267
            // don't bother here.
268
            return;
269
        }
270
271
        if ($response->getContent()) {
272
            $contentType = current(explode(';', $response->headers->get('content-type', 'text/html')));
273
            $response->setContent(
274
                $this->aliasing->mapContent(
275
                    $contentType,
276
                    UrlMapperInterface::MODE_INTERNAL_TO_PUBLIC,
277
                    $response->getContent(),
278
                    [$request->getHttpHost()]
279
                )
280
            );
281
        }
282
    }
283
}
284