| 3 |
liveuser |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
namespace Guzzle\Http;
|
|
|
4 |
|
|
|
5 |
use Guzzle\Common\Event;
|
|
|
6 |
use Guzzle\Http\Exception\BadResponseException;
|
|
|
7 |
use Guzzle\Http\Url;
|
|
|
8 |
use Guzzle\Http\Message\Response;
|
|
|
9 |
use Guzzle\Http\Message\RequestInterface;
|
|
|
10 |
use Guzzle\Http\Message\RequestFactory;
|
|
|
11 |
use Guzzle\Http\Message\EntityEnclosingRequestInterface;
|
|
|
12 |
use Guzzle\Http\Exception\TooManyRedirectsException;
|
|
|
13 |
use Guzzle\Http\Exception\CouldNotRewindStreamException;
|
|
|
14 |
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
|
15 |
|
|
|
16 |
/**
|
|
|
17 |
* Plugin to implement HTTP redirects. Can redirect like a web browser or using strict RFC 2616 compliance
|
|
|
18 |
*/
|
|
|
19 |
class RedirectPlugin implements EventSubscriberInterface
|
|
|
20 |
{
|
|
|
21 |
const REDIRECT_COUNT = 'redirect.count';
|
|
|
22 |
const MAX_REDIRECTS = 'redirect.max';
|
|
|
23 |
const STRICT_REDIRECTS = 'redirect.strict';
|
|
|
24 |
const PARENT_REQUEST = 'redirect.parent_request';
|
|
|
25 |
const DISABLE = 'redirect.disable';
|
|
|
26 |
|
|
|
27 |
/**
|
|
|
28 |
* @var int Default number of redirects allowed when no setting is supplied by a request
|
|
|
29 |
*/
|
|
|
30 |
protected $defaultMaxRedirects = 5;
|
|
|
31 |
|
|
|
32 |
public static function getSubscribedEvents()
|
|
|
33 |
{
|
|
|
34 |
return array(
|
|
|
35 |
'request.sent' => array('onRequestSent', 100),
|
|
|
36 |
'request.clone' => 'cleanupRequest',
|
|
|
37 |
'request.before_send' => 'cleanupRequest'
|
|
|
38 |
);
|
|
|
39 |
}
|
|
|
40 |
|
|
|
41 |
/**
|
|
|
42 |
* Clean up the parameters of a request when it is cloned
|
|
|
43 |
*
|
|
|
44 |
* @param Event $event Event emitted
|
|
|
45 |
*/
|
|
|
46 |
public function cleanupRequest(Event $event)
|
|
|
47 |
{
|
|
|
48 |
$params = $event['request']->getParams();
|
|
|
49 |
unset($params[self::REDIRECT_COUNT]);
|
|
|
50 |
unset($params[self::PARENT_REQUEST]);
|
|
|
51 |
}
|
|
|
52 |
|
|
|
53 |
/**
|
|
|
54 |
* Called when a request receives a redirect response
|
|
|
55 |
*
|
|
|
56 |
* @param Event $event Event emitted
|
|
|
57 |
*/
|
|
|
58 |
public function onRequestSent(Event $event)
|
|
|
59 |
{
|
|
|
60 |
$response = $event['response'];
|
|
|
61 |
$request = $event['request'];
|
|
|
62 |
|
|
|
63 |
// Only act on redirect requests with Location headers
|
|
|
64 |
if (!$response || $request->getParams()->get(self::DISABLE)) {
|
|
|
65 |
return;
|
|
|
66 |
}
|
|
|
67 |
|
|
|
68 |
// Trace the original request based on parameter history
|
|
|
69 |
$original = $this->getOriginalRequest($request);
|
|
|
70 |
|
|
|
71 |
// Terminating condition to set the effective response on the original request
|
|
|
72 |
if (!$response->isRedirect() || !$response->hasHeader('Location')) {
|
|
|
73 |
if ($request !== $original) {
|
|
|
74 |
// This is a terminating redirect response, so set it on the original request
|
|
|
75 |
$response->getParams()->set(self::REDIRECT_COUNT, $original->getParams()->get(self::REDIRECT_COUNT));
|
|
|
76 |
$original->setResponse($response);
|
|
|
77 |
$response->setEffectiveUrl($request->getUrl());
|
|
|
78 |
}
|
|
|
79 |
return;
|
|
|
80 |
}
|
|
|
81 |
|
|
|
82 |
$this->sendRedirectRequest($original, $request, $response);
|
|
|
83 |
}
|
|
|
84 |
|
|
|
85 |
/**
|
|
|
86 |
* Get the original request that initiated a series of redirects
|
|
|
87 |
*
|
|
|
88 |
* @param RequestInterface $request Request to get the original request from
|
|
|
89 |
*
|
|
|
90 |
* @return RequestInterface
|
|
|
91 |
*/
|
|
|
92 |
protected function getOriginalRequest(RequestInterface $request)
|
|
|
93 |
{
|
|
|
94 |
$original = $request;
|
|
|
95 |
// The number of redirects is held on the original request, so determine which request that is
|
|
|
96 |
while ($parent = $original->getParams()->get(self::PARENT_REQUEST)) {
|
|
|
97 |
$original = $parent;
|
|
|
98 |
}
|
|
|
99 |
|
|
|
100 |
return $original;
|
|
|
101 |
}
|
|
|
102 |
|
|
|
103 |
/**
|
|
|
104 |
* Create a redirect request for a specific request object
|
|
|
105 |
*
|
|
|
106 |
* Takes into account strict RFC compliant redirection (e.g. redirect POST with POST) vs doing what most clients do
|
|
|
107 |
* (e.g. redirect POST with GET).
|
|
|
108 |
*
|
|
|
109 |
* @param RequestInterface $request Request being redirected
|
|
|
110 |
* @param RequestInterface $original Original request
|
|
|
111 |
* @param int $statusCode Status code of the redirect
|
|
|
112 |
* @param string $location Location header of the redirect
|
|
|
113 |
*
|
|
|
114 |
* @return RequestInterface Returns a new redirect request
|
|
|
115 |
* @throws CouldNotRewindStreamException If the body needs to be rewound but cannot
|
|
|
116 |
*/
|
|
|
117 |
protected function createRedirectRequest(
|
|
|
118 |
RequestInterface $request,
|
|
|
119 |
$statusCode,
|
|
|
120 |
$location,
|
|
|
121 |
RequestInterface $original
|
|
|
122 |
) {
|
|
|
123 |
$redirectRequest = null;
|
|
|
124 |
$strict = $original->getParams()->get(self::STRICT_REDIRECTS);
|
|
|
125 |
|
|
|
126 |
// Switch method to GET for 303 redirects. 301 and 302 redirects also switch to GET unless we are forcing RFC
|
|
|
127 |
// compliance to emulate what most browsers do. NOTE: IE only switches methods on 301/302 when coming from a POST.
|
|
|
128 |
if ($request instanceof EntityEnclosingRequestInterface && ($statusCode == 303 || (!$strict && $statusCode <= 302))) {
|
|
|
129 |
$redirectRequest = RequestFactory::getInstance()->cloneRequestWithMethod($request, 'GET');
|
|
|
130 |
} else {
|
|
|
131 |
$redirectRequest = clone $request;
|
|
|
132 |
}
|
|
|
133 |
|
|
|
134 |
$redirectRequest->setIsRedirect(true);
|
|
|
135 |
// Always use the same response body when redirecting
|
|
|
136 |
$redirectRequest->setResponseBody($request->getResponseBody());
|
|
|
137 |
|
|
|
138 |
$location = Url::factory($location);
|
|
|
139 |
// If the location is not absolute, then combine it with the original URL
|
|
|
140 |
if (!$location->isAbsolute()) {
|
|
|
141 |
$originalUrl = $redirectRequest->getUrl(true);
|
|
|
142 |
// Remove query string parameters and just take what is present on the redirect Location header
|
|
|
143 |
$originalUrl->getQuery()->clear();
|
|
|
144 |
$location = $originalUrl->combine((string) $location, true);
|
|
|
145 |
}
|
|
|
146 |
|
|
|
147 |
$redirectRequest->setUrl($location);
|
|
|
148 |
|
|
|
149 |
// Add the parent request to the request before it sends (make sure it's before the onRequestClone event too)
|
|
|
150 |
$redirectRequest->getEventDispatcher()->addListener(
|
|
|
151 |
'request.before_send',
|
|
|
152 |
$func = function ($e) use (&$func, $request, $redirectRequest) {
|
|
|
153 |
$redirectRequest->getEventDispatcher()->removeListener('request.before_send', $func);
|
|
|
154 |
$e['request']->getParams()->set(RedirectPlugin::PARENT_REQUEST, $request);
|
|
|
155 |
}
|
|
|
156 |
);
|
|
|
157 |
|
|
|
158 |
// Rewind the entity body of the request if needed
|
|
|
159 |
if ($redirectRequest instanceof EntityEnclosingRequestInterface && $redirectRequest->getBody()) {
|
|
|
160 |
$body = $redirectRequest->getBody();
|
|
|
161 |
// Only rewind the body if some of it has been read already, and throw an exception if the rewind fails
|
|
|
162 |
if ($body->ftell() && !$body->rewind()) {
|
|
|
163 |
throw new CouldNotRewindStreamException(
|
|
|
164 |
'Unable to rewind the non-seekable entity body of the request after redirecting. cURL probably '
|
|
|
165 |
. 'sent part of body before the redirect occurred. Try adding acustom rewind function using on the '
|
|
|
166 |
. 'entity body of the request using setRewindFunction().'
|
|
|
167 |
);
|
|
|
168 |
}
|
|
|
169 |
}
|
|
|
170 |
|
|
|
171 |
return $redirectRequest;
|
|
|
172 |
}
|
|
|
173 |
|
|
|
174 |
/**
|
|
|
175 |
* Prepare the request for redirection and enforce the maximum number of allowed redirects per client
|
|
|
176 |
*
|
|
|
177 |
* @param RequestInterface $original Original request
|
|
|
178 |
* @param RequestInterface $request Request to prepare and validate
|
|
|
179 |
* @param Response $response The current response
|
|
|
180 |
*
|
|
|
181 |
* @return RequestInterface
|
|
|
182 |
*/
|
|
|
183 |
protected function prepareRedirection(RequestInterface $original, RequestInterface $request, Response $response)
|
|
|
184 |
{
|
|
|
185 |
$params = $original->getParams();
|
|
|
186 |
// This is a new redirect, so increment the redirect counter
|
|
|
187 |
$current = $params[self::REDIRECT_COUNT] + 1;
|
|
|
188 |
$params[self::REDIRECT_COUNT] = $current;
|
|
|
189 |
// Use a provided maximum value or default to a max redirect count of 5
|
|
|
190 |
$max = isset($params[self::MAX_REDIRECTS]) ? $params[self::MAX_REDIRECTS] : $this->defaultMaxRedirects;
|
|
|
191 |
|
|
|
192 |
// Throw an exception if the redirect count is exceeded
|
|
|
193 |
if ($current > $max) {
|
|
|
194 |
$this->throwTooManyRedirectsException($original, $max);
|
|
|
195 |
return false;
|
|
|
196 |
} else {
|
|
|
197 |
// Create a redirect request based on the redirect rules set on the request
|
|
|
198 |
return $this->createRedirectRequest(
|
|
|
199 |
$request,
|
|
|
200 |
$response->getStatusCode(),
|
|
|
201 |
trim($response->getLocation()),
|
|
|
202 |
$original
|
|
|
203 |
);
|
|
|
204 |
}
|
|
|
205 |
}
|
|
|
206 |
|
|
|
207 |
/**
|
|
|
208 |
* Send a redirect request and handle any errors
|
|
|
209 |
*
|
|
|
210 |
* @param RequestInterface $original The originating request
|
|
|
211 |
* @param RequestInterface $request The current request being redirected
|
|
|
212 |
* @param Response $response The response of the current request
|
|
|
213 |
*
|
|
|
214 |
* @throws BadResponseException|\Exception
|
|
|
215 |
*/
|
|
|
216 |
protected function sendRedirectRequest(RequestInterface $original, RequestInterface $request, Response $response)
|
|
|
217 |
{
|
|
|
218 |
// Validate and create a redirect request based on the original request and current response
|
|
|
219 |
if ($redirectRequest = $this->prepareRedirection($original, $request, $response)) {
|
|
|
220 |
try {
|
|
|
221 |
$redirectRequest->send();
|
|
|
222 |
} catch (BadResponseException $e) {
|
|
|
223 |
$e->getResponse();
|
|
|
224 |
if (!$e->getResponse()) {
|
|
|
225 |
throw $e;
|
|
|
226 |
}
|
|
|
227 |
}
|
|
|
228 |
}
|
|
|
229 |
}
|
|
|
230 |
|
|
|
231 |
/**
|
|
|
232 |
* Throw a too many redirects exception for a request
|
|
|
233 |
*
|
|
|
234 |
* @param RequestInterface $original Request
|
|
|
235 |
* @param int $max Max allowed redirects
|
|
|
236 |
*
|
|
|
237 |
* @throws TooManyRedirectsException when too many redirects have been issued
|
|
|
238 |
*/
|
|
|
239 |
protected function throwTooManyRedirectsException(RequestInterface $original, $max)
|
|
|
240 |
{
|
|
|
241 |
$original->getEventDispatcher()->addListener(
|
|
|
242 |
'request.complete',
|
|
|
243 |
$func = function ($e) use (&$func, $original, $max) {
|
|
|
244 |
$original->getEventDispatcher()->removeListener('request.complete', $func);
|
|
|
245 |
$str = "{$max} redirects were issued for this request:\n" . $e['request']->getRawHeaders();
|
|
|
246 |
throw new TooManyRedirectsException($str);
|
|
|
247 |
}
|
|
|
248 |
);
|
|
|
249 |
}
|
|
|
250 |
}
|