Blame | Last modification | View Log | RSS feed
<?phpnamespace Guzzle\Http;use Guzzle\Common\Event;use Guzzle\Http\Exception\BadResponseException;use Guzzle\Http\Url;use Guzzle\Http\Message\Response;use Guzzle\Http\Message\RequestInterface;use Guzzle\Http\Message\RequestFactory;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. Can redirect like a web browser or using strict RFC 2616 compliance*/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';const DISABLE = 'redirect.disable';/*** @var int Default number of redirects allowed when no setting is supplied by a request*/protected $defaultMaxRedirects = 5;public static function getSubscribedEvents(){return array('request.sent' => array('onRequestSent', 100),'request.clone' => 'cleanupRequest','request.before_send' => 'cleanupRequest');}/*** Clean up the parameters of a request when it is cloned** @param Event $event Event emitted*/public function cleanupRequest(Event $event){$params = $event['request']->getParams();unset($params[self::REDIRECT_COUNT]);unset($params[self::PARENT_REQUEST]);}/*** Called when a request receives a redirect response** @param Event $event Event emitted*/public function onRequestSent(Event $event){$response = $event['response'];$request = $event['request'];// Only act on redirect requests with Location headersif (!$response || $request->getParams()->get(self::DISABLE)) {return;}// Trace the original request based on parameter history$original = $this->getOriginalRequest($request);// Terminating condition to set the effective response on the original requestif (!$response->isRedirect() || !$response->hasHeader('Location')) {if ($request !== $original) {// This is a terminating redirect response, so set it on the original request$response->getParams()->set(self::REDIRECT_COUNT, $original->getParams()->get(self::REDIRECT_COUNT));$original->setResponse($response);$response->setEffectiveUrl($request->getUrl());}return;}$this->sendRedirectRequest($original, $request, $response);}/*** Get the original request that initiated a series of redirects** @param RequestInterface $request Request to get the original request from** @return RequestInterface*/protected function getOriginalRequest(RequestInterface $request){$original = $request;// The number of redirects is held on the original request, so determine which request that iswhile ($parent = $original->getParams()->get(self::PARENT_REQUEST)) {$original = $parent;}return $original;}/*** Create a redirect request for a specific request object** Takes into account strict RFC compliant redirection (e.g. redirect POST with POST) vs doing what most clients do* (e.g. redirect POST with GET).** @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);// Switch method to GET for 303 redirects. 301 and 302 redirects also switch to GET unless we are forcing RFC// compliance to emulate what most browsers do. NOTE: IE only switches methods on 301/302 when coming from a POST.if ($request instanceof EntityEnclosingRequestInterface && ($statusCode == 303 || (!$strict && $statusCode <= 302))) {$redirectRequest = RequestFactory::getInstance()->cloneRequestWithMethod($request, 'GET');} else {$redirectRequest = clone $request;}$redirectRequest->setIsRedirect(true);// Always use the same response body when redirecting$redirectRequest->setResponseBody($request->getResponseBody());$location = Url::factory($location);// If the location is not absolute, then combine it with the original URLif (!$location->isAbsolute()) {$originalUrl = $redirectRequest->getUrl(true);// Remove query string parameters and just take what is present on the redirect Location header$originalUrl->getQuery()->clear();$location = $originalUrl->combine((string) $location, true);}$redirectRequest->setUrl($location);// Add the parent request to the request before it sends (make sure it's before the onRequestClone event too)$redirectRequest->getEventDispatcher()->addListener('request.before_send',$func = function ($e) use (&$func, $request, $redirectRequest) {$redirectRequest->getEventDispatcher()->removeListener('request.before_send', $func);$e['request']->getParams()->set(RedirectPlugin::PARENT_REQUEST, $request);});// Rewind the entity body of the request if neededif ($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 failsif ($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;}/*** Prepare the request for redirection and enforce the maximum number of allowed redirects per client** @param RequestInterface $original Original request* @param RequestInterface $request Request to prepare and validate* @param Response $response The current response** @return RequestInterface*/protected function prepareRedirection(RequestInterface $original, RequestInterface $request, Response $response){$params = $original->getParams();// This is a new redirect, so increment the redirect counter$current = $params[self::REDIRECT_COUNT] + 1;$params[self::REDIRECT_COUNT] = $current;// Use a provided maximum value or default to a max redirect count of 5$max = isset($params[self::MAX_REDIRECTS]) ? $params[self::MAX_REDIRECTS] : $this->defaultMaxRedirects;// Throw an exception if the redirect count is exceededif ($current > $max) {$this->throwTooManyRedirectsException($original, $max);return false;} else {// Create a redirect request based on the redirect rules set on the requestreturn $this->createRedirectRequest($request,$response->getStatusCode(),trim($response->getLocation()),$original);}}/*** Send a redirect request and handle any errors** @param RequestInterface $original The originating request* @param RequestInterface $request The current request being redirected* @param Response $response The response of the current request** @throws BadResponseException|\Exception*/protected function sendRedirectRequest(RequestInterface $original, RequestInterface $request, Response $response){// Validate and create a redirect request based on the original request and current responseif ($redirectRequest = $this->prepareRedirection($original, $request, $response)) {try {$redirectRequest->send();} catch (BadResponseException $e) {$e->getResponse();if (!$e->getResponse()) {throw $e;}}}}/*** Throw a too many redirects exception for a request** @param RequestInterface $original Request* @param int $max Max allowed redirects** @throws TooManyRedirectsException when too many redirects have been issued*/protected function throwTooManyRedirectsException(RequestInterface $original, $max){$original->getEventDispatcher()->addListener('request.complete',$func = function ($e) use (&$func, $original, $max) {$original->getEventDispatcher()->removeListener('request.complete', $func);$str = "{$max} redirects were issued for this request:\n" . $e['request']->getRawHeaders();throw new TooManyRedirectsException($str);});}}