/
RedirectPlugin.php
206 lines (180 loc) · 7.91 KB
/
RedirectPlugin.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
<?php
namespace Guzzle\Http;
use Guzzle\Common\Event;
use Guzzle\Http\Message\Response;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\EntityEnclosingRequestInterface;
use Guzzle\Http\Exception\TooManyRedirectsException;
use Guzzle\Http\Exception\CouldNotRewindStreamException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Plugin to implement HTTP redirects
*/
class RedirectPlugin implements EventSubscriberInterface
{
const REDIRECT_COUNT = 'redirect.count';
const MAX_REDIRECTS = 'redirect.max';
const STRICT_REDIRECTS = 'redirect.strict';
const PARENT_REQUEST = 'redirect.parent_request';
/**
* @var int Default number of redirects allowed when no setting is supplied by a request
*/
protected $defaultMaxRedirects = 5;
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
'request.sent' => array('onRequestSent', 100),
'request.clone' => 'onRequestClone'
);
}
/**
* Clean up the parameters of a request when it is cloned
*
* @param Event $event Event emitted
*/
public function onRequestClone(Event $event)
{
$event['request']->getParams()->remove(self::REDIRECT_COUNT)->remove(self::PARENT_REQUEST);
}
/**
* Called when a request receives a redirect response
*
* @param Event $event Event emitted
*/
public function onRequestSent(Event $event)
{
$response = $event['response'];
// Only act on redirect requests with Location headers
if (!$response || !$response->isRedirect() || !$response->hasHeader('Location')) {
return;
}
$request = $event['request'];
$orignalRequest = $this->prepareRedirection($request);
// Create a redirect request based on the redirect rules set on the request
$redirectRequest = $this->createRedirectRequest(
$request,
$event['response']->getStatusCode(),
trim($response->getHeader('Location')),
$orignalRequest
);
// Send the redirect request and hijack the response of the original request
$redirectResponse = $redirectRequest->send();
$redirectResponse->setPreviousResponse($event['response']);
$request->setResponse($redirectResponse);
}
/**
* Create a redirect request for a specific request object, taking into account strict redirection vs doing what
* most clients do
*
* @param RequestInterface $request Request being redirected
* @param RequestInterface $original Original request
* @param int $statusCode Status code of the redirect
* @param string $location Location header of the redirect
*
* @return RequestInterface Returns a new redirect request
* @throws CouldNotRewindStreamException If the body needs to be rewound but cannot
*/
protected function createRedirectRequest(
RequestInterface $request,
$statusCode,
$location,
RequestInterface $original
) {
$redirectRequest = null;
$strict = $original->getParams()->get(self::STRICT_REDIRECTS);
// Use a GET request if this is an entity enclosing request and we are not forcing RFC compliance, but rather
// emulating what all browsers would do
if ($request instanceof EntityEnclosingRequestInterface && !$strict && $statusCode <= 302) {
$redirectRequest = $this->cloneRequestWithGetMethod($request);
} else {
$redirectRequest = clone $request;
}
$redirectRequest->setUrl($redirectRequest->getUrl(true)->combine($location));
$redirectRequest->getParams()->set(self::PARENT_REQUEST, $request);
// Rewind the entity body of the request if needed
if ($redirectRequest instanceof EntityEnclosingRequestInterface && $redirectRequest->getBody()) {
$body = $redirectRequest->getBody();
// Only rewind the body if some of it has been read already, and throw an exception if the rewind fails
if ($body->ftell() && !$body->rewind()) {
throw new CouldNotRewindStreamException(
'Unable to rewind the non-seekable entity body of the request after redirecting. cURL probably '
. 'sent part of body before the redirect occurred. Try adding acustom rewind function using on the '
. 'entity body of the request using setRewindFunction().'
);
}
}
return $redirectRequest;
}
/**
* Clone a request while changing the method to GET. Emulates the behavior of
* {@see Guzzle\Http\Message\Request::clone}, but can change the HTTP method.
*
* @param EntityEnclosingRequestInterface $request Request to clone
*
* @return RequestInterface Returns a GET request
*/
protected function cloneRequestWithGetMethod(EntityEnclosingRequestInterface $request)
{
// Create a new GET request using the original request's URL
$redirectRequest = $request->getClient()->get($request->getUrl());
$redirectRequest->getCurlOptions()->replace($request->getCurlOptions()->getAll());
// Copy over the headers, while ensuring that the Content-Length is not copied
$redirectRequest->setHeaders($request->getHeaders()->getAll())->removeHeader('Content-Length');
$redirectRequest->setEventDispatcher(clone $request->getEventDispatcher());
$redirectRequest->getParams()
->replace($request->getParams()->getAll())
->remove('curl_handle')->remove('queued_response')->remove('curl_multi');
return $redirectRequest;
}
/**
* Prepare the request for redirection and enforce the maximum number of allowed redirects per client
*
* @param RequestInterface $request Request to prepare and validate
*
* @return RequestInterface Returns the original request
*/
protected function prepareRedirection(RequestInterface $request)
{
$original = $request;
// The number of redirects is held on the original request, so determine which request that is
while ($parent = $original->getParams()->get(self::PARENT_REQUEST)) {
$original = $parent;
}
if ($parent = $request->getParams()->get(self::PARENT_REQUEST)) {
$request->getResponse()->setPreviousResponse($parent->getResponse());
}
$params = $original->getParams();
$current = $params->get(self::REDIRECT_COUNT) + 1;
$params->set(self::REDIRECT_COUNT, $current);
// Use a provided maximum value or default to a max redirect count of 5
$max = $params->hasKey(self::MAX_REDIRECTS)
? $params->get(self::MAX_REDIRECTS)
: $this->defaultMaxRedirects;
// Throw an exception if the redirect count is exceeded
if ($current > $max) {
$this->throwTooManyRedirectsException($request);
}
return $original;
}
/**
* Throw a too many redirects exception for a request
*
* @param RequestInterface $request Request
* @throws TooManyRedirectsException when too many redirects have been issued
*/
protected function throwTooManyRedirectsException(RequestInterface $request)
{
$responses = array();
// Create a nice message to use when throwing the exception
do {
$response = $request->getResponse();
$responses[] = '> ' . $request->getRawHeaders() . "\n\n< " . $response->getRawHeaders();
$request = $response->getPreviousResponse() ? $response->getPreviousResponse()->getRequest() : null;
} while ($request);
$transaction = implode("* Sending redirect request\n", array_reverse($responses));
throw new TooManyRedirectsException("Too many redirects were issued for this transaction:\n{$transaction}");
}
}