Completed
Pull Request — master (#10)
by Tortue
34:14
created

Turbolinks   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 269
Duplicated Lines 8.92 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

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

13 Methods

Rating   Name   Duplication   Size   Complexity  
A decorateResponse() 0 14 4
B redirectTo() 0 14 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 ($turbolinks !== false && $request->isXmlHttpRequest() && ! $request->isMethod('GET')) {
99
            $location = $response->headers->get(self::ORIGIN_RESPONSE_HEADER);
100
            $turbolinksContent = $this->visitLocationWithTurbolinks($location, $turbolinks);
101
            $this->performTurbolinksResponse($request, $response, $turbolinksContent);
102
        } elseif ($this->canHandleRedirect($request)) {
103
            $this->storeTurbolinksLocationInSession($request, $response);
104
        }
105
106
        return $response;
107
    }
108
109
    /**
110
     * Checks if the request can handle a Turbolink redirect. You need to have a
111
     * session and a XHR request header to handle a redirect.
112
     *
113
     * @param  Request $request
114
     *
115
     * @return bool
116
     */
117
    private function canHandleRedirect(Request $request)
118
    {
119
        $session = $request->getSession();
120
        return $session instanceof SessionInterface && $request->headers->has(self::ORIGIN_REQUEST_HEADER);
121
    }
122
123
    /**
124
     * Parse the given url into an origin array with the scheme, host and port.
125
     *
126
     * @param  string $url
127
     *
128
     * @return array
129
     */
130
    private function getUrlOrigin($url)
131
    {
132
        return array(
133
            parse_url($url, PHP_URL_SCHEME),
134
            parse_url($url, PHP_URL_HOST),
135
            parse_url($url, PHP_URL_PORT),
136
        );
137
    }
138
139
    /**
140
     * Checks if the request and the response have the same origin.
141
     *
142
     * @param  Request  $request
143
     * @param  Response $response
144
     *
145
     * @return bool
146
     */
147
    private function haveSameOrigin(Request $request, Response $response)
148
    {
149
        $requestOrigin = $this->getUrlOrigin($request->headers->get(self::ORIGIN_REQUEST_HEADER));
150
        $responseOrigin = $this->getUrlOrigin($response->headers->get(self::ORIGIN_RESPONSE_HEADER));
151
152
        return $requestOrigin == $responseOrigin;
153
    }
154
155
    /**
156
     * Modifies the response status code. Checks for cross domain redirects and
157
     * blocks them.
158
     *
159
     * @param Request  $request
160
     * @param Response $response
161
     */
162
    private function modifyStatusCode(Request $request, Response $response)
163
    {
164
        if ($request->headers->has(self::ORIGIN_REQUEST_HEADER)
165
            && $response->headers->has(self::ORIGIN_RESPONSE_HEADER)
166
            && !$this->haveSameOrigin($request, $response)
167
        ) {
168
            $response->setStatusCode(Response::HTTP_FORBIDDEN);
169
        }
170
    }
171
172
    /**
173
     * @param  ResponseHeaderBag $headers
174
     * @return mixed
175
     */
176
    private function extractTurbolinksOptions($headers)
177
    {
178
        $options = $this->extractTurbolinksHeaders($headers);
179
180
        // Equivalent of the `array_pull()` Laravel helper:
181
        //   $turbolinks = array_pull($options, 'turbolinks');
182
        // See: http://laravel.com/docs/5.1/helpers#method-array-pull
183
        $turbolinks = null;
184
        if (isset($options['turbolinks'])) {
185
            $turbolinks = $options['turbolinks'];
186
            unset($options['turbolinks']);
187
        }
188
189
        return $turbolinks;
190
    }
191
192
    private function visitLocationWithTurbolinks($location, $action)
193
    {
194
        $visitOptions = array(
195
          'action' => (string) $action === "advance" ? $action : "replace"
196
        );
197
198
        $script = array();
199
        $script[] = "Turbolinks.clearCache();";
200
        $script[] = "Turbolinks.visit(".json_encode($location, JSON_UNESCAPED_SLASHES).", ".json_encode($visitOptions).");";
201
202
        return implode(PHP_EOL, $script);
203
    }
204
205
    /**
206
     * @param Request  $request
207
     * @param Response $response
208
     */
209 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...
210
    {
211
        // Stores the return value (the redirect target url) to persist through to the redirect
212
        // request, where it will be used to set the Turbolinks-Location response header. The
213
        // Turbolinks script will detect the header and use replaceState to reflect the redirected
214
        // url.
215
        $session = $request->getSession();
216
217
        if ($session) {
218
            $location = $response->headers->get(self::ORIGIN_RESPONSE_HEADER);
219
            $session->set(self::LOCATION_SESSION_ATTR_NAME, $location);
220
        }
221
    }
222
223
    /**
224
     * @param Request  $request
225
     * @param Response $response
226
     * @param string   $body Content of the response
227
     */
228
    private function performTurbolinksResponse(Request $request, Response $response, $body)
229
    {
230
        $response->headers->set('Content-Type', $request->getMimeType('js'));
231
        $response->setStatusCode(200);
232
        $response->setContent($body);
233
    }
234
235
    /**
236
     * @param Request  $request
237
     * @param Response $response
238
     */
239 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...
240
    {
241
        $session = $request->getSession();
242
243
        // set 'Turbolinks-Location' header
244
        if ($session && $session->has(self::LOCATION_SESSION_ATTR_NAME)) {
245
            $response->headers->add(
246
                array(self::REDIRECT_RESPONSE_HEADER => $session->remove(self::LOCATION_SESSION_ATTR_NAME))
247
            );
248
        }
249
    }
250
251
    /**
252
     * @param  ResponseHeaderBag $headers
253
     *
254
     * @return array Turbolinks options
255
     */
256
    public function extractTurbolinksHeaders($headers)
257
    {
258
        $options = array();
259
        $optionsMap = self::$turbolinksOptionsMap;
260
261
        foreach ($headers as $key => $value) {
262
            if ($result = array_search($key, array_map('strtolower', $optionsMap))) {
263
                $options[$result] = $value;
264
                if (is_array($headers)) {
265
                    unset($headers[$key]);
266
                } elseif ($headers instanceof ResponseHeaderBag) {
267
                    $headers->remove($key);
268
                }
269
            }
270
        }
271
272
        return $options;
273
    }
274
275
    /**
276
     * Return HTTP headers equivalent of the given Turbolinks options.
277
     * E.G. `['turbolinks'  => 'advance']` becomes `['X-Turbolinks' => 'advance']`
278
     *
279
     * @param  array $options
280
     *
281
     * @return array
282
     */
283
    public function convertTurbolinksOptions($options = array())
284
    {
285
        $headers = array();
286
        $optionsMap = self::$turbolinksOptionsMap;
287
288
        foreach ($options as $key => $value) {
289
            if (in_array($key, array_keys($optionsMap))) {
290
                $headers[$optionsMap[$key]] = $value;
291
            }
292
        }
293
294
        return $headers;
295
    }
296
}
297