Skip to content

Commit e3bbfa4

Browse files
committed
Merge pull request #8 from webfactory/forward-cookies-and-headers
Add annotations to allow forwarding legacy headers/cookies
2 parents 8b6f9ab + 7d05d1d commit e3bbfa4

File tree

6 files changed

+272
-13
lines changed

6 files changed

+272
-13
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
/*
3+
* (c) webfactory GmbH <info@webfactory.de>
4+
*
5+
* For the full copyright and license information, please view the LICENSE
6+
* file that was distributed with this source code.
7+
*/
8+
9+
namespace Webfactory\Bundle\LegacyIntegrationBundle\Integration\Annotation;
10+
11+
/**
12+
* @Annotation
13+
*/
14+
class KeepCookies
15+
{
16+
public $value;
17+
18+
public function shouldKeep($name) {
19+
return $this->value === null || in_array($name, $this->value);
20+
}
21+
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
/*
3+
* (c) webfactory GmbH <info@webfactory.de>
4+
*
5+
* For the full copyright and license information, please view the LICENSE
6+
* file that was distributed with this source code.
7+
*/
8+
9+
namespace Webfactory\Bundle\LegacyIntegrationBundle\Integration\Annotation;
10+
11+
/**
12+
* @Annotation
13+
*/
14+
class KeepHeaders
15+
{
16+
public $value;
17+
18+
public function shouldKeep($name) {
19+
return $this->value === null || in_array($name, $this->value);
20+
}
21+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
/*
3+
* (c) webfactory GmbH <info@webfactory.de>
4+
*
5+
* For the full copyright and license information, please view the LICENSE
6+
* file that was distributed with this source code.
7+
*/
8+
9+
namespace Webfactory\Bundle\LegacyIntegrationBundle\Integration\Filter;
10+
11+
use Doctrine\Common\Annotations\Reader;
12+
use Symfony\Component\HttpFoundation\Cookie;
13+
use Symfony\Component\HttpFoundation\Response;
14+
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
15+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
16+
use Webfactory\Bundle\LegacyIntegrationBundle\Integration\Annotation\KeepCookies;
17+
use Webfactory\Bundle\LegacyIntegrationBundle\Integration\Annotation\KeepHeaders;
18+
use Webfactory\Bundle\LegacyIntegrationBundle\Integration\Filter as FilterInterface;
19+
20+
class KeepCookiesAndHeadersFilter implements FilterInterface
21+
{
22+
/** @var Reader */
23+
private $reader;
24+
25+
/** @var Response */
26+
private $legacyResponse;
27+
28+
/** @var KeepHeaders */
29+
private $keepHeadersAnnotation;
30+
31+
/** @var KeepCookies */
32+
private $keepCookiesAnnotation;
33+
34+
public function __construct(Reader $reader)
35+
{
36+
$this->reader = $reader;
37+
}
38+
39+
public function filter(FilterControllerEvent $event, Response $response)
40+
{
41+
if (!is_array($controller = $event->getController())) {
42+
return;
43+
}
44+
45+
$this->legacyResponse = $response;
46+
47+
$object = new \ReflectionObject($controller[0]);
48+
$method = $object->getMethod($controller[1]);
49+
50+
foreach ($this->reader->getMethodAnnotations($method) as $annotation) {
51+
if ($annotation instanceof KeepHeaders) {
52+
$this->keepHeadersAnnotation = $annotation;
53+
} else if ($annotation instanceof KeepCookies) {
54+
$this->keepCookiesAnnotation = $annotation;
55+
}
56+
}
57+
}
58+
59+
public function onKernelResponse(FilterResponseEvent $event)
60+
{
61+
if (!$this->legacyResponse) {
62+
return;
63+
}
64+
65+
$response = $event->getResponse();
66+
$legacyHeaders = $this->legacyResponse->headers;
67+
68+
if ($this->keepHeadersAnnotation) {
69+
foreach ($legacyHeaders->all() as $name => $values) {
70+
if ($this->keepHeadersAnnotation->shouldKeep($name)) {
71+
foreach ($values as $value) {
72+
$response->headers->set($name, $value);
73+
}
74+
}
75+
}
76+
}
77+
78+
if ($this->keepCookiesAnnotation) {
79+
foreach ($legacyHeaders->getCookies() as $cookie) {
80+
/** @var Cookie $cookie */
81+
if ($this->keepCookiesAnnotation->shouldKeep($cookie->getName())) {
82+
$response->headers->setCookie($cookie);
83+
}
84+
}
85+
}
86+
}
87+
}

Integration/LegacyCaptureResponseFactory.php

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,27 @@
88

99
namespace Webfactory\Bundle\LegacyIntegrationBundle\Integration;
1010

11+
use Symfony\Component\HttpFoundation\Cookie;
1112
use Symfony\Component\HttpFoundation\RedirectResponse;
1213

1314
use Symfony\Component\HttpFoundation\Response;
1415

1516
class LegacyCaptureResponseFactory
1617
{
18+
private static $dateFormats = array(
19+
'D, d M Y H:i:s T',
20+
'D, d-M-y H:i:s T',
21+
'D, d-M-Y H:i:s T',
22+
'D, d-m-y H:i:s T',
23+
'D, d-m-Y H:i:s T',
24+
'D M j G:i:s Y',
25+
'D M d H:i:s Y T',
26+
);
1727

1828
public static function create($legacyExecutionCallback)
1929
{
2030
ob_start();
21-
22-
$statusCode = call_user_func($legacyExecutionCallback) ? : 200;
31+
$statusCode = call_user_func($legacyExecutionCallback) ?: 200;
2332

2433
if (function_exists('http_response_code')) {
2534
$statusCode = http_response_code();
@@ -36,19 +45,122 @@ public static function create($legacyExecutionCallback)
3645
header_remove();
3746

3847
$responseHeaders = array();
48+
$cookies = array();
3949

4050
foreach ($headers as $header) {
4151
$header = preg_match('(^([^:]+):(.*)$)', $header, $matches);
42-
$headerName = trim($matches[1]);
52+
$headerName = strtolower(trim($matches[1]));
4353
$headerValue = trim($matches[2]);
44-
$responseHeaders[$headerName][] = $headerValue;
54+
55+
if ($headerName == 'set-cookie') {
56+
$cookies[] = self::createCookieFromString($headerValue);
57+
} else {
58+
$responseHeaders[$headerName][] = $headerValue;
59+
}
60+
}
61+
62+
if (isset($responseHeaders['location'])) {
63+
unset($responseHeaders['expires']);
64+
$response = new RedirectResponse($responseHeaders['location'][0], 302, $responseHeaders);
65+
} else {
66+
$response = new Response($content, $statusCode, $responseHeaders);
67+
}
68+
69+
foreach ($cookies as $cookie) {
70+
$response->headers->setCookie($cookie);
71+
}
72+
73+
return $response;
74+
}
75+
76+
private static function createCookieFromString($cookie, $url = null)
77+
{
78+
$parts = explode(';', $cookie);
79+
80+
if (false === strpos($parts[0], '=')) {
81+
throw new \InvalidArgumentException(sprintf('The cookie string "%s" is not valid.', $parts[0]));
82+
}
83+
84+
list($name, $value) = explode('=', array_shift($parts), 2);
85+
86+
$values = array(
87+
'name' => trim($name),
88+
'value' => trim($value),
89+
'expires' => 0,
90+
'path' => '/',
91+
'domain' => '',
92+
'secure' => false,
93+
'httponly' => false,
94+
'passedRawValue' => true,
95+
);
96+
97+
if (null !== $url) {
98+
if ((false === $urlParts = parse_url($url)) || !isset($urlParts['host'])) {
99+
throw new \InvalidArgumentException(sprintf('The URL "%s" is not valid.', $url));
100+
}
101+
102+
$values['domain'] = $urlParts['host'];
103+
$values['path'] = isset($urlParts['path']) ? substr($urlParts['path'], 0, strrpos($urlParts['path'], '/')) : '';
104+
}
105+
106+
foreach ($parts as $part) {
107+
$part = trim($part);
108+
109+
if ('secure' === strtolower($part)) {
110+
// Ignore the secure flag if the original URI is not given or is not HTTPS
111+
if (!$url || !isset($urlParts['scheme']) || 'https' != $urlParts['scheme']) {
112+
continue;
113+
}
114+
115+
$values['secure'] = true;
116+
117+
continue;
118+
}
119+
120+
if ('httponly' === strtolower($part)) {
121+
$values['httponly'] = true;
122+
123+
continue;
124+
}
125+
126+
if (2 === count($elements = explode('=', $part, 2))) {
127+
if ('expires' === strtolower($elements[0])) {
128+
$elements[1] = self::parseDate($elements[1]);
129+
}
130+
131+
$values[strtolower($elements[0])] = $elements[1];
132+
}
133+
}
134+
135+
return new Cookie(
136+
$values['name'],
137+
$values['value'],
138+
$values['expires'],
139+
$values['path'],
140+
$values['domain'],
141+
$values['secure'],
142+
$values['httponly']
143+
);
144+
}
145+
146+
private static function parseDate($dateValue)
147+
{
148+
// trim single quotes around date if present
149+
if (($length = strlen($dateValue)) > 1 && "'" === $dateValue[0] && "'" === $dateValue[$length - 1]) {
150+
$dateValue = substr($dateValue, 1, -1);
151+
}
152+
153+
foreach (self::$dateFormats as $dateFormat) {
154+
if (false !== $date = \DateTime::createFromFormat($dateFormat, $dateValue, new \DateTimeZone('GMT'))) {
155+
return $date->getTimestamp();
156+
}
45157
}
46158

47-
if (isset($responseHeaders['Location'])) {
48-
unset($responseHeaders['Expires']);
49-
return new RedirectResponse($responseHeaders['Location'][0], 302, $responseHeaders);
159+
// attempt a fallback for unusual formatting
160+
if (false !== $date = date_create($dateValue, new \DateTimeZone('GMT'))) {
161+
return $date->getTimestamp();
50162
}
51163

52-
return new Response($content, $statusCode, $responseHeaders);
164+
throw new \InvalidArgumentException(sprintf('Could not parse date "%s".', $dateValue));
53165
}
54166
}

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ You must provide a single file that can be `include()`ed in order to run your
5151
legacy applcation. Typically you will already have this - it should be your
5252
legacy application's front controller.
5353

54-
This file should _return_ the HTTP status code sent by the legacy application.
55-
This is because the `http_response_code()` function is not available until PHP 5.4.
54+
If you are running PHP < 5.4, this file should _return_ the HTTP status code
55+
sent by the legacy application. Starting with PHP 5.4, `http_response_code()`
56+
will be used to detect it.
5657

5758
Also, as this bundle will try to capture the response including headers using
5859
output buffering, you must not flush the response body or headers
@@ -83,8 +84,17 @@ class MyController ...
8384
}
8485
```
8586

86-
There are two ways of mixing your legacy world with your new world: Either you create a new layout and embed parts of
87-
the legacy application, or you retain your old layout and embed new parts in it.
87+
This will run your legacy application before entering the controller. The entire output including
88+
HTTP headers (repeat after me: including HTTP headers) will be captured and saved. *Nothing* will
89+
be sent to the client unless you take care of doing so.
90+
91+
Regarding the legacy response body, there are two ways of mixing your legacy world with your new world: Either you create a new layout and embed parts of
92+
the legacy application, or you retain your old layout and embed new parts in it. The following sections
93+
explain both of them.
94+
95+
Regarding HTTP headers and especially cookies sent by the legacy application make sure
96+
you don't miss the filters explained further below. For example, if your legacy code uses `session_start()`
97+
you probably need to forward the session cookie.
8898

8999
### Using XPath to embed parts of the legacy response in your new layout
90100

@@ -183,7 +193,8 @@ In particular,
183193
- @Legacy\Passthru will send the legacy application's response as-is, so the controller itself will never be run
184194
- @Legacy\IgnoreRedirect will bypass the controller if the legacy application sent a Location: redirect header.
185195
- @Legacy\IgnoreHeader("some-name") will bypass the controller if the legacy application sent "Some-Name:" header. This can be used to make the legacy application control execution of the Symfony2 controller (use with caution).
186-
196+
- @Legacy\KeepHeaders will apply *all* HTTP headers found in the legacy response and add them to the response created by Symfony controller. You can also selectively pick headers via `@Legacy\KeepHeaders({"X-Some-Header", "X-Some-Other"})`
197+
- @Legacy\KeepCookies works like `KeepHeaders` but peeks at cookie names.
187198

188199
Bugs
189200
---

Resources/config/services.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
<tag name="webfactory_legacy_integration.filter"/>
2525
</service>
2626

27+
<service class="Webfactory\Bundle\LegacyIntegrationBundle\Integration\Filter\KeepCookiesAndHeadersFilter">
28+
<argument type="service" id="annotation_reader"/>
29+
<tag name="webfactory_legacy_integration.filter"/>
30+
<tag name="kernel.event_listener" event="kernel.response"/>
31+
</service>
32+
2733
<service class="Webfactory\Bundle\LegacyIntegrationBundle\Twig\Extension">
2834
<argument type="service" id="service_container"/>
2935
<tag name="twig.extension"/>

0 commit comments

Comments
 (0)