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) |
|
|
|
|
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) |
|
|
|
|
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
|
|
|
|
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.