Test Failed
Pull Request — release/2.x (#61)
by
unknown
21:54 queued 12:25
created

Listener::onKernelResponse()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 50
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 48.2875

Importance

Changes 7
Bugs 2 Features 0
Metric Value
cc 9
eloc 20
c 7
b 2
f 0
nc 20
nop 1
dl 0
loc 50
ccs 3
cts 14
cp 0.2143
crap 48.2875
rs 8.0555
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
    const SLASH_SUFFIX_ABSTAIN = 'abstain';
25
    const SLASH_SUFFIX_ACCEPT = 'accept';
26
    const SLASH_SUFFIX_REDIRECT_PERM = 'redirect-301';
27
    const SLASH_SUFFIX_REDIRECT_TEMP = 'redirect-302';
28
29
    /** @var Aliasing */
30
    protected $aliasing;
31
32
    /** @var RouterListener */
33
    protected $router;
34
35 11
    /** @var string[] */
36
    protected $excludePatterns = array();
37 11
38 11
    /** @var bool */
39 11
    protected $isParamsEnabled = false;
40
41
    /** @var string|null */
42
    protected $slashSuffixHandling = self::SLASH_SUFFIX_ABSTAIN;
43
44
    /**
45
     * Construct the aliasing listener.
46
     *
47
     * @param Aliasing $aliasing
48
     * @param RouterListener $router
49
     */
50
    public function __construct(Aliasing $aliasing, RouterListener $router)
51
    {
52
        $this->aliasing = $aliasing;
53
        $this->router = $router;
54
    }
55
56
    /**
57
     * Listens to redirect responses, to replace any internal url with a public one.
58
     *
59
     * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $e
60
     * @return void
61
     */
62
    public function onKernelResponse(Event\FilterResponseEvent $e)
63
    {
64
        if ($e->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
65
            $response = $e->getResponse();
66
67
            // only do anything if the response has a Location header
68
            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

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