Completed
Pull Request — master (#10)
by Tortue
02:04
created

Turbolinks   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 272
Duplicated Lines 8.82 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 11
Bugs 1 Features 1
Metric Value
wmc 35
c 11
b 1
f 1
lcom 1
cbo 5
dl 24
loc 272
rs 9

13 Methods

Rating   Name   Duplication   Size   Complexity  
A decorateResponse() 0 14 4
B redirectTo() 0 17 5
A canHandleRedirect() 0 5 2
A getUrlOrigin() 0 8 1
A haveSameOrigin() 0 7 1
A modifyStatusCode() 0 9 4
A extractTurbolinksOptions() 0 15 2
A visitLocationWithTurbolinks() 0 12 2
A storeTurbolinksLocationInSession() 13 13 2
A performTurbolinksResponse() 0 6 1
A setTurbolinksLocationHeaderFromSession() 11 11 3
B extractTurbolinksHeaders() 0 18 5
A convertTurbolinksOptions() 0 13 3

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
/*
4
 * This file is part of the Helthe Turbolinks package.
5
 *
6
 * (c) Carl Alexander <[email protected]>
7
 * (c) Tortue Torche <[email protected]>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace Helthe\Component\Turbolinks;
14
15
use Symfony\Component\HttpFoundation\Request;
16
use Symfony\Component\HttpFoundation\Response;
17
use Symfony\Component\HttpFoundation\RedirectResponse;
18
use Symfony\Component\HttpFoundation\Session\SessionInterface;
19
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
20
21
/**
22
 * Turbolinks implements the server-side logic expected by Turbolinks javascript.
23
 *
24
 * @link https://github.com/rails/turbolinks/blob/master/lib/turbolinks.rb
25
 *
26
 * @author Carl Alexander <[email protected]>
27
 */
28
class Turbolinks
29
{
30
    /**
31
     * Request header used for origin validation.
32
     *
33
     * @var string
34
     */
35
    const ORIGIN_REQUEST_HEADER = 'Turbolinks-Referrer';
36
37
    /**
38
     * Response header used for origin validation.
39
     *
40
     * @var string
41
     */
42
    const ORIGIN_RESPONSE_HEADER = 'Location';
43
44
    /**
45
     * Redirect header inserted in the response.
46
     *
47
     * @var string
48
     */
49
    const REDIRECT_RESPONSE_HEADER = 'Turbolinks-Location';
50
51
    /**
52
     * Session attribute name for the redirect location.
53
     *
54
     * @var string
55
     */
56
    const LOCATION_SESSION_ATTR_NAME = 'helthe_turbolinks_location';
57
58
    /**
59
     * @var array
60
     */
61
    public static $turbolinksOptionsMap = array(
62
        // Handles normal redirection with Turbolinks, if not set to `false`.
63
        // Possible values: `null`, `'replace'`, `'advance'` or `false`
64
        'turbolinks' => 'X-Turbolinks',
65
    );
66
67
    /**
68
     * Modifies the HTTP headers and status code of the Response so that it can be
69
     * properly handled by the Turbolinks javascript.
70
     *
71
     * @param Request  $request
72
     * @param Response $response
73
     */
74
    public function decorateResponse(Request $request, Response $response)
75
    {
76
        if ($request->headers->has(self::ORIGIN_REQUEST_HEADER)) {
77
            $request->headers->set('referer', $request->headers->get(self::ORIGIN_REQUEST_HEADER));
78
        }
79
80
        $this->setTurbolinksLocationHeaderFromSession($request, $response);
81
82
        if ($response->isRedirect() && $response->headers->has(self::ORIGIN_RESPONSE_HEADER)) {
83
            $this->redirectTo($request, $response);
84
        }
85
86
        $this->modifyStatusCode($request, $response);
87
    }
88
89
    /**
90
     * @param Request  $request
91
     * @param Response $response
92
     * @return \Symfony\Component\HttpFoundation\Response
93
     */
94
    public function redirectTo($request, $response)
95
    {
96
        $turbolinks = $this->extractTurbolinksOptions($response->headers);
97
98
        if (
99
            (array) $turbolinks !== array(false) &&
100
            $request->isXmlHttpRequest() && ! $request->isMethod('GET')
101
        ) {
102
            $location = $response->headers->get(self::ORIGIN_RESPONSE_HEADER);
103
            $turbolinksContent = $this->visitLocationWithTurbolinks($location, $turbolinks);
104
            $this->performTurbolinksResponse($request, $response, $turbolinksContent);
105
        } elseif ($this->canHandleRedirect($request)) {
106
            $this->storeTurbolinksLocationInSession($request, $response);
107
        }
108
109
        return $response;
110
    }
111
112
    /**
113
     * Checks if the request can handle a Turbolink redirect. You need to have a
114
     * session and a XHR request header to handle a redirect.
115
     *
116
     * @param  Request $request
117
     *
118
     * @return bool
119
     */
120
    private function canHandleRedirect(Request $request)
121
    {
122
        $session = $request->getSession();
123
        return $session instanceof SessionInterface && $request->headers->has(self::ORIGIN_REQUEST_HEADER);
124
    }
125
126
    /**
127
     * Parse the given url into an origin array with the scheme, host and port.
128
     *
129
     * @param  string $url
130
     *
131
     * @return array
132
     */
133
    private function getUrlOrigin($url)
134
    {
135
        return array(
136
            parse_url($url, PHP_URL_SCHEME),
137
            parse_url($url, PHP_URL_HOST),
138
            parse_url($url, PHP_URL_PORT),
139
        );
140
    }
141
142
    /**
143
     * Checks if the request and the response have the same origin.
144
     *
145
     * @param  Request  $request
146
     * @param  Response $response
147
     *
148
     * @return bool
149
     */
150
    private function haveSameOrigin(Request $request, Response $response)
151
    {
152
        $requestOrigin = $this->getUrlOrigin($request->headers->get(self::ORIGIN_REQUEST_HEADER));
153
        $responseOrigin = $this->getUrlOrigin($response->headers->get(self::ORIGIN_RESPONSE_HEADER));
154
155
        return $requestOrigin == $responseOrigin;
156
    }
157
158
    /**
159
     * Modifies the response status code. Checks for cross domain redirects and
160
     * blocks them.
161
     *
162
     * @param Request  $request
163
     * @param Response $response
164
     */
165
    private function modifyStatusCode(Request $request, Response $response)
166
    {
167
        if ($request->headers->has(self::ORIGIN_REQUEST_HEADER)
168
            && $response->headers->has(self::ORIGIN_RESPONSE_HEADER)
169
            && !$this->haveSameOrigin($request, $response)
170
        ) {
171
            $response->setStatusCode(Response::HTTP_FORBIDDEN);
172
        }
173
    }
174
175
    /**
176
     * @param  ResponseHeaderBag $headers
177
     * @return mixed
178
     */
179
    private function extractTurbolinksOptions($headers)
180
    {
181
        $options = $this->extractTurbolinksHeaders($headers);
182
183
        // Equivalent of the `array_pull()` Laravel helper:
184
        //   $turbolinks = array_pull($options, 'turbolinks');
185
        // See: http://laravel.com/docs/5.1/helpers#method-array-pull
186
        $turbolinks = null;
187
        if (isset($options['turbolinks'])) {
188
            $turbolinks = $options['turbolinks'];
189
            unset($options['turbolinks']);
190
        }
191
192
        return $turbolinks;
193
    }
194
195
    private function visitLocationWithTurbolinks($location, $action)
196
    {
197
        $visitOptions = array(
198
          'action' => (string) $action === "advance" ? $action : "replace"
199
        );
200
201
        $script = array();
202
        $script[] = "Turbolinks.clearCache();";
203
        $script[] = "Turbolinks.visit(".json_encode($location, JSON_UNESCAPED_SLASHES).", ".json_encode($visitOptions).");";
204
205
        return implode(PHP_EOL, $script);
206
    }
207
208
    /**
209
     * @param Request  $request
210
     * @param Response $response
211
     */
212 View Code Duplication
    private function storeTurbolinksLocationInSession(Request $request, Response $response)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
213
    {
214
        // Stores the return value (the redirect target url) to persist through to the redirect
215
        // request, where it will be used to set the Turbolinks-Location response header. The
216
        // Turbolinks script will detect the header and use replaceState to reflect the redirected
217
        // url.
218
        $session = $request->getSession();
219
220
        if ($session) {
221
            $location = $response->headers->get(self::ORIGIN_RESPONSE_HEADER);
222
            $session->set(self::LOCATION_SESSION_ATTR_NAME, $location);
223
        }
224
    }
225
226
    /**
227
     * @param Request  $request
228
     * @param Response $response
229
     * @param string   $body Content of the response
230
     */
231
    private function performTurbolinksResponse(Request $request, Response $response, $body)
232
    {
233
        $response->headers->set('Content-Type', $request->getMimeType('js'));
234
        $response->setStatusCode(200);
235
        $response->setContent($body);
236
    }
237
238
    /**
239
     * @param Request  $request
240
     * @param Response $response
241
     */
242 View Code Duplication
    private function setTurbolinksLocationHeaderFromSession(Request $request, Response $response)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
243
    {
244
        $session = $request->getSession();
245
246
        // set 'Turbolinks-Location' header
247
        if ($session && $session->has(self::LOCATION_SESSION_ATTR_NAME)) {
248
            $response->headers->add(
249
                array(self::REDIRECT_RESPONSE_HEADER => $session->remove(self::LOCATION_SESSION_ATTR_NAME))
250
            );
251
        }
252
    }
253
254
    /**
255
     * @param  ResponseHeaderBag $headers
256
     *
257
     * @return array Turbolinks options
258
     */
259
    public function extractTurbolinksHeaders($headers)
260
    {
261
        $options = array();
262
        $optionsMap = self::$turbolinksOptionsMap;
263
264
        foreach ($headers as $key => $value) {
265
            if ($result = array_search($key, array_map('strtolower', $optionsMap))) {
266
                $options[$result] = $value;
267
                if (is_array($headers)) {
268
                    unset($headers[$key]);
269
                } elseif ($headers instanceof ResponseHeaderBag) {
270
                    $headers->remove($key);
271
                }
272
            }
273
        }
274
275
        return $options;
276
    }
277
278
    /**
279
     * Return HTTP headers equivalent of the given Turbolinks options.
280
     * E.G. `['turbolinks'  => 'advance']` becomes `['X-Turbolinks' => 'advance']`
281
     *
282
     * @param  array $options
283
     *
284
     * @return array
285
     */
286
    public function convertTurbolinksOptions($options = array())
287
    {
288
        $headers = array();
289
        $optionsMap = self::$turbolinksOptionsMap;
290
291
        foreach ($options as $key => $value) {
292
            if (in_array($key, array_keys($optionsMap))) {
293
                $headers[$optionsMap[$key]] = $value;
294
            }
295
        }
296
297
        return $headers;
298
    }
299
}
300