Passed
Push — feature/multisite-poc ( f9befe )
by Erik
11:53
created

Listener::retrieveSite()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Zicht Online <http://zicht.nl>
4
 */
5
6
namespace Zicht\Bundle\UrlBundle\Aliasing;
7
8
use Symfony\Component\HttpFoundation\Request;
9
use Symfony\Component\HttpFoundation\Response;
10
use Symfony\Component\HttpKernel\Event;
11
use Symfony\Component\HttpFoundation\RedirectResponse;
12
use Symfony\Component\HttpKernel\EventListener\RouterListener;
13
use Symfony\Component\HttpKernel\HttpKernelInterface;
14
use Zicht\Bundle\UrlBundle\Aliasing\Mapper\UrlMapperInterface;
15
use Zicht\Bundle\UrlBundle\Url\Params\UriParser;
16
use Zicht\Bundle\UrlBundle\Entity\UrlAlias;
17
18
/**
19
 * Listens to incoming and outgoing requests to handle url aliasing at the kernel master request level.
20
 */
21
class Listener
22
{
23
    protected $aliasing;
24
25
    protected $excludePatterns = [];
26
    protected $isParamsEnabled = false;
27
28
    /**
29
     * Construct the aliasing listener.
30
     *
31
     * @param Aliasing $aliasing
32
     * @param RouterListener $router
33
     */
34
    public function __construct(Aliasing $aliasing, RouterListener $router)
35
    {
36
        $this->aliasing = $aliasing;
37
        $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...
38
    }
39
40
    /**
41
     * Listens to redirect responses, to replace any internal url with a public one.
42
     *
43
     * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $e
44
     * @return void
45
     */
46
    public function onKernelResponse(Event\FilterResponseEvent $e)
47
    {
48
        if ($e->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
49
            $response = $e->getResponse();
50
51
            // only do anything if the response has a Location header
52
            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

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