Passed
Push — feature/ZICHTDEV-1090 ( c794f4 )
by Erik
13:47
created

Listener::onKernelController()   A

Complexity

Conditions 3
Paths 5

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 7
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 10
ccs 0
cts 10
cp 0
crap 12
rs 10
1
<?php
2
/**
3
 * @copyright Zicht Online <http://zicht.nl>
4
 */
5
6
namespace Zicht\Bundle\UrlBundle\Aliasing;
7
8
use Doctrine\Common\Annotations\AnnotationReader;
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\Annotation\Headers;
17
use Zicht\Bundle\UrlBundle\Url\Params\UriParser;
18
use Zicht\Bundle\UrlBundle\Entity\UrlAlias;
19
20
/**
21
 * Listens to incoming and outgoing requests to handle url aliasing at the kernel master request level.
22
 */
23
class Listener
24
{
25
    protected $aliasing;
26
27
    protected $excludePatterns = [];
28
    protected $isParamsEnabled = false;
29
    protected $customHeaders = [];
30
31
    /** @var AnnotationReader */
32
    protected $annotationReader;
33
34
    /**
35
     * Construct the aliasing listener.
36
     *
37
     * @param Aliasing $aliasing
38
     * @param RouterListener $router
39
     */
40
    public function __construct(Aliasing $aliasing, RouterListener $router)
41
    {
42
        $this->aliasing = $aliasing;
43
        $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...
44
        $this->annotationReader = new AnnotationReader();
45
    }
46
47
    public function onKernelController(Event\FilterControllerEvent $event)
48
    {
49
        list($controller, $methodName) = $event->getController();
50
        $reflectionObject = new \ReflectionObject($controller);
51
        try {
52
            $reflectionMethod = $reflectionObject->getMethod($methodName);
53
            if (null !== $methodAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Headers::class)) {
54
                $this->customHeaders = $methodAnnotation->getHeaders();
55
            }
56
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
57
58
        }
59
    }
60
61
    /**
62
     * Listens to redirect responses, to replace any internal url with a public one.
63
     *
64
     * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $e
65
     * @return void
66
     */
67
    public function onKernelResponse(Event\FilterResponseEvent $e)
68
    {
69
        if ($e->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
70
            $response = $e->getResponse();
71
72
            // only do anything if the response has a Location header
73
            if (false !== ($location = $response->headers->get('location', false))) {
0 ignored issues
show
introduced by
The condition false !== $location = $r...>get('location', false) is always true.
Loading history...
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

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