These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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\ResponseHeaderBag; |
||
19 | |||
20 | /** |
||
21 | * Turbolinks implements the server-side logic expected by Turbolinks javascript. |
||
22 | * |
||
23 | * @link https://github.com/rails/turbolinks/blob/master/lib/turbolinks.rb |
||
24 | * |
||
25 | * @author Carl Alexander <[email protected]> |
||
26 | */ |
||
27 | class Turbolinks |
||
28 | { |
||
29 | /** |
||
30 | * Request header used for origin validation. |
||
31 | * |
||
32 | * @var string |
||
33 | */ |
||
34 | const ORIGIN_REQUEST_HEADER = 'Turbolinks-Referrer'; |
||
35 | |||
36 | /** |
||
37 | * Response header used for origin validation. |
||
38 | * |
||
39 | * @var string |
||
40 | */ |
||
41 | const ORIGIN_RESPONSE_HEADER = 'Location'; |
||
42 | |||
43 | /** |
||
44 | * Redirect header inserted in the response. |
||
45 | * |
||
46 | * @var string |
||
47 | */ |
||
48 | const REDIRECT_RESPONSE_HEADER = 'Turbolinks-Location'; |
||
49 | |||
50 | /** |
||
51 | * Session attribute name for the redirect location. |
||
52 | * |
||
53 | * @var string |
||
54 | */ |
||
55 | const LOCATION_SESSION_ATTR_NAME = 'helthe_turbolinks_location'; |
||
56 | |||
57 | /** |
||
58 | * @var array |
||
59 | */ |
||
60 | public static $turbolinksOptionsMap = array( |
||
61 | // Handles normal redirection with Turbolinks, if not set to `false`. |
||
62 | // Possible values: `null`, `'replace'`, `'advance'` or `false` |
||
63 | 'turbolinks' => 'X-Turbolinks', |
||
64 | ); |
||
65 | |||
66 | /** |
||
67 | * Modifies the HTTP headers and status code of the Response so that it can be |
||
68 | * properly handled by the Turbolinks javascript. |
||
69 | * |
||
70 | * @param Request $request |
||
71 | * @param Response $response |
||
72 | */ |
||
73 | public function decorateResponse(Request $request, Response $response) |
||
74 | { |
||
75 | if ($request->headers->has(self::ORIGIN_REQUEST_HEADER)) { |
||
76 | $request->headers->set('referer', $request->headers->get(self::ORIGIN_REQUEST_HEADER)); |
||
0 ignored issues
–
show
|
|||
77 | } |
||
78 | |||
79 | $this->setTurbolinksLocationHeaderFromSession($request, $response); |
||
80 | |||
81 | if ($response->isRedirect() && $response->headers->has(self::ORIGIN_RESPONSE_HEADER)) { |
||
82 | $this->redirectTo($request, $response); |
||
83 | } |
||
84 | |||
85 | $this->modifyStatusCode($request, $response); |
||
86 | } |
||
87 | |||
88 | /** |
||
89 | * @param Request $request |
||
90 | * @param Response $response |
||
91 | * @return \Symfony\Component\HttpFoundation\Response |
||
92 | */ |
||
93 | public function redirectTo($request, $response) |
||
94 | { |
||
95 | $turbolinks = $this->extractTurbolinksOptions($response->headers); |
||
96 | |||
97 | if ( |
||
98 | $turbolinks !== false && |
||
99 | $request->isXmlHttpRequest() && ! $request->isMethod('GET') |
||
100 | ) { |
||
101 | $location = $response->headers->get(self::ORIGIN_RESPONSE_HEADER); |
||
102 | $turbolinksContent = $this->visitLocationWithTurbolinks($location, $turbolinks); |
||
103 | $this->performTurbolinksResponse($request, $response, $turbolinksContent); |
||
104 | } elseif ($this->canHandleRedirect($request)) { |
||
105 | $this->storeTurbolinksLocationInSession($request, $response); |
||
106 | } |
||
107 | |||
108 | return $response; |
||
109 | } |
||
110 | |||
111 | /** |
||
112 | * Checks if the request can handle a Turbolink redirect. You need to have a |
||
113 | * session and a XHR request header to handle a redirect. |
||
114 | * |
||
115 | * @param Request $request |
||
116 | * |
||
117 | * @return bool |
||
118 | */ |
||
119 | private function canHandleRedirect(Request $request) |
||
120 | { |
||
121 | $session = $request->getSession(); |
||
122 | return (is_a($session, '\Symfony\Component\HttpFoundation\Session\SessionInterface') || is_a($session, '\Illuminate\Contracts\Session\Session')) && |
||
123 | $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)); |
||
0 ignored issues
–
show
It seems like
$request->headers->get(s...:ORIGIN_REQUEST_HEADER) targeting Symfony\Component\HttpFoundation\HeaderBag::get() can also be of type array or null ; however, Helthe\Component\Turboli...bolinks::getUrlOrigin() does only seem to accept string , maybe add an additional type check?
This check looks at variables that are passed out again to other methods. If the outgoing method call has stricter type requirements than the method itself, an issue is raised. An additional type check may prevent trouble. ![]() |
|||
153 | $responseOrigin = $this->getUrlOrigin($response->headers->get(self::ORIGIN_RESPONSE_HEADER)); |
||
0 ignored issues
–
show
It seems like
$response->headers->get(...ORIGIN_RESPONSE_HEADER) targeting Symfony\Component\HttpFoundation\HeaderBag::get() can also be of type array or null ; however, Helthe\Component\Turboli...bolinks::getUrlOrigin() does only seem to accept string , maybe add an additional type check?
This check looks at variables that are passed out again to other methods. If the outgoing method call has stricter type requirements than the method itself, an issue is raised. An additional type check may prevent trouble. ![]() |
|||
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' => is_string($action) && $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 | private function storeTurbolinksLocationInSession(Request $request, Response $response) |
||
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 | if ($session) { |
||
220 | $location = $response->headers->get(self::ORIGIN_RESPONSE_HEADER); |
||
221 | $setMethod = method_exists($session, 'put') ? 'put' : 'set'; |
||
222 | $session->$setMethod(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->remove('Location'); |
||
234 | $response->headers->set('Content-Type', $request->getMimeType('js')); |
||
235 | $response->setStatusCode(200); |
||
236 | $response->setContent($body); |
||
237 | } |
||
238 | |||
239 | /** |
||
240 | * @param Request $request |
||
241 | * @param Response $response |
||
242 | */ |
||
243 | private function setTurbolinksLocationHeaderFromSession(Request $request, Response $response) |
||
244 | { |
||
245 | $session = $request->getSession(); |
||
246 | |||
247 | // set 'Turbolinks-Location' header |
||
248 | if ($session && $session->has(self::LOCATION_SESSION_ATTR_NAME)) { |
||
249 | $location = $session->remove(self::LOCATION_SESSION_ATTR_NAME); |
||
250 | $response->headers->add( |
||
251 | array(self::REDIRECT_RESPONSE_HEADER => $location) |
||
252 | ); |
||
253 | } |
||
254 | } |
||
255 | |||
256 | /** |
||
257 | * @param ResponseHeaderBag $headers |
||
258 | * |
||
259 | * @return array Turbolinks options |
||
260 | */ |
||
261 | public function extractTurbolinksHeaders($headers) |
||
262 | { |
||
263 | $options = array(); |
||
264 | $optionsMap = self::$turbolinksOptionsMap; |
||
265 | |||
266 | foreach ($headers as $key => $value) { |
||
267 | if ($result = array_search($key, array_map('strtolower', $optionsMap))) { |
||
268 | if (is_array($value) && count($value) === 1 && array_key_exists(0, $value)) { |
||
269 | $value = $value[0]; |
||
270 | } |
||
271 | $options[$result] = $value; |
||
272 | if (is_array($headers)) { |
||
273 | unset($headers[$key]); |
||
274 | } elseif ($headers instanceof ResponseHeaderBag) { |
||
275 | $headers->remove($key); |
||
276 | } |
||
277 | } |
||
278 | } |
||
279 | |||
280 | return $options; |
||
281 | } |
||
282 | |||
283 | /** |
||
284 | * Return HTTP headers equivalent of the given Turbolinks options. |
||
285 | * E.G. `['turbolinks' => 'advance']` becomes `['X-Turbolinks' => 'advance']` |
||
286 | * |
||
287 | * @param array $options |
||
288 | * |
||
289 | * @return array |
||
290 | */ |
||
291 | public function convertTurbolinksOptions($options = array()) |
||
292 | { |
||
293 | $headers = array(); |
||
294 | $optionsMap = self::$turbolinksOptionsMap; |
||
295 | |||
296 | foreach ($options as $key => $value) { |
||
297 | if (in_array($key, array_keys($optionsMap))) { |
||
298 | $headers[$optionsMap[$key]] = $value; |
||
299 | } |
||
300 | } |
||
301 | |||
302 | return $headers; |
||
303 | } |
||
304 | } |
||
305 |
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.