Overview

Namespaces

  • Contrib
    • Bundle
      • CoverallsBundle
        • Console
        • Entity
      • CoverallsV1Bundle
        • Api
        • Collector
        • Command
        • Config
        • Entity
          • Git
    • Component
      • File
      • Log
      • System
        • Git
  • Guzzle
    • Batch
      • Exception
    • Cache
    • Common
      • Exception
    • Http
      • Curl
      • Exception
      • Message
      • QueryAggregator
    • Inflection
    • Iterator
    • Log
    • Parser
      • Cookie
      • Message
      • UriTemplate
      • Url
    • Plugin
      • Async
      • Backoff
      • Cache
      • Cookie
        • CookieJar
        • Exception
      • CurlAuth
      • ErrorResponse
        • Exception
      • History
      • Log
      • Md5
      • Mock
      • Oauth
    • Service
      • Builder
      • Command
        • Factory
        • LocationVisitor
          • Request
          • Response
      • Description
      • Exception
      • Resource
    • Stream
  • PHP
  • Psr
    • Log
  • Symfony
    • Component
      • Config
        • Definition
          • Builder
          • Exception
        • Exception
        • Loader
        • Resource
        • Util
      • Console
        • Command
        • Formatter
        • Helper
        • Input
        • Output
        • Tester
      • EventDispatcher
        • Debug
      • Finder
        • Adapter
        • Comparator
        • Exception
        • Expression
        • Iterator
        • Shell
      • Stopwatch
      • Yaml
        • Exception

Classes

  • AbstractEntityBodyDecorator
  • CachingEntityBody
  • Client
  • EntityBody
  • IoEmittingEntityBody
  • Mimetypes
  • QueryString
  • ReadLimitEntityBody
  • RedirectPlugin
  • Url

Interfaces

  • ClientInterface
  • EntityBodyInterface
  • Overview
  • Namespace
  • Class
  • Tree
  • Todo
  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:     /**
 33:      * {@inheritdoc}
 34:      */
 35:     public static function getSubscribedEvents()
 36:     {
 37:         return array(
 38:             'request.sent'  => array('onRequestSent', 100),
 39:             'request.clone' => 'onRequestClone'
 40:         );
 41:     }
 42: 
 43:     /**
 44:      * Clean up the parameters of a request when it is cloned
 45:      *
 46:      * @param Event $event Event emitted
 47:      */
 48:     public function onRequestClone(Event $event)
 49:     {
 50:         $event['request']->getParams()->remove(self::REDIRECT_COUNT)->remove(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 || !$response->isRedirect() || !$response->hasHeader('Location')
 65:             || $request->getParams()->get(self::DISABLE)
 66:         ) {
 67:             return;
 68:         }
 69: 
 70:         // Prepare the request for a redirect and grab the original request that started the transaction
 71:         $originalRequest = $this->prepareRedirection($request);
 72: 
 73:         // Create a redirect request based on the redirect rules set on the request
 74:         $redirectRequest = $this->createRedirectRequest(
 75:             $request,
 76:             $event['response']->getStatusCode(),
 77:             trim($response->getHeader('Location')),
 78:             $originalRequest
 79:         );
 80: 
 81:         try {
 82:             // Send the redirect request and hijack the response of the original request
 83:             $redirectResponse = $redirectRequest->send();
 84:         } catch (BadResponseException $e) {
 85:             // Still hijack if an exception occurs after redirecting
 86:             $redirectResponse = $e->getResponse();
 87:             if (!$e->getResponse()) {
 88:                 throw $e;
 89:             }
 90:         }
 91: 
 92:         $request->setResponse($redirectResponse);
 93:         if (!$redirectResponse->getPreviousResponse()) {
 94:             $redirectResponse->setPreviousResponse($response);
 95:         }
 96:     }
 97: 
 98:     /**
 99:      * Create a redirect request for a specific request object
100:      *
101:      * Takes into account strict RFC compliant redirection (e.g. redirect POST with POST) vs doing what most clients do
102:      * (e.g. redirect POST with GET).
103:      *
104:      * @param RequestInterface $request    Request being redirected
105:      * @param RequestInterface $original   Original request
106:      * @param int              $statusCode Status code of the redirect
107:      * @param string           $location   Location header of the redirect
108:      *
109:      * @return RequestInterface Returns a new redirect request
110:      * @throws CouldNotRewindStreamException If the body needs to be rewound but cannot
111:      */
112:     protected function createRedirectRequest(
113:         RequestInterface $request,
114:         $statusCode,
115:         $location,
116:         RequestInterface $original
117:     ) {
118:         $redirectRequest = null;
119:         $strict = $original->getParams()->get(self::STRICT_REDIRECTS);
120:         // Use a GET request if this is an entity enclosing request and we are not forcing RFC compliance, but rather
121:         // emulating what all browsers would do
122:         if ($request instanceof EntityEnclosingRequestInterface && !$strict && $statusCode <= 302) {
123:             $redirectRequest = $this->cloneRequestWithGetMethod($request);
124:         } else {
125:             $redirectRequest = clone $request;
126:         }
127: 
128:         $redirectRequest->setIsRedirect(true);
129:         // Always use the same response body when redirecting
130:         $redirectRequest->setResponseBody($request->getResponseBody());
131: 
132:         $location = Url::factory($location);
133:         // If the location is not absolute, then combine it with the original URL
134:         if (!$location->isAbsolute()) {
135:             $originalUrl = $redirectRequest->getUrl(true);
136:             // Remove query string parameters and just take what is present on the redirect Location header
137:             $originalUrl->getQuery()->clear();
138:             $location = $originalUrl->combine((string) $location);
139:         }
140: 
141:         $redirectRequest->setUrl($location);
142:         $redirectRequest->getParams()->set(self::PARENT_REQUEST, $request);
143: 
144:         // Rewind the entity body of the request if needed
145:         if ($redirectRequest instanceof EntityEnclosingRequestInterface && $redirectRequest->getBody()) {
146:             $body = $redirectRequest->getBody();
147:             // Only rewind the body if some of it has been read already, and throw an exception if the rewind fails
148:             if ($body->ftell() && !$body->rewind()) {
149:                 throw new CouldNotRewindStreamException(
150:                     'Unable to rewind the non-seekable entity body of the request after redirecting. cURL probably '
151:                     . 'sent part of body before the redirect occurred. Try adding acustom rewind function using on the '
152:                     . 'entity body of the request using setRewindFunction().'
153:                 );
154:             }
155:         }
156: 
157:         return $redirectRequest;
158:     }
159: 
160:     /**
161:      * Clone a request while changing the method to GET. Emulates the behavior of
162:      * {@see Guzzle\Http\Message\Request::clone}, but can change the HTTP method.
163:      *
164:      * @param EntityEnclosingRequestInterface $request Request to clone
165:      *
166:      * @return RequestInterface Returns a GET request
167:      */
168:     protected function cloneRequestWithGetMethod(EntityEnclosingRequestInterface $request)
169:     {
170:         return RequestFactory::getInstance()->cloneRequestWithMethod($request, 'GET');
171:     }
172: 
173:     /**
174:      * Prepare the request for redirection and enforce the maximum number of allowed redirects per client
175:      *
176:      * @param RequestInterface $request Request to prepare and validate
177:      *
178:      * @return RequestInterface Returns the original request
179:      */
180:     protected function prepareRedirection(RequestInterface $request)
181:     {
182:         $original = $request;
183:         // The number of redirects is held on the original request, so determine which request that is
184:         while ($parent = $original->getParams()->get(self::PARENT_REQUEST)) {
185:             $original = $parent;
186:         }
187: 
188:         // Always associate the parent response with the current response so that a chain can be established
189:         if ($parent = $request->getParams()->get(self::PARENT_REQUEST)) {
190:             $request->getResponse()->setPreviousResponse($parent->getResponse());
191:         }
192: 
193:         $params = $original->getParams();
194:         // This is a new redirect, so increment the redirect counter
195:         $current = $params->get(self::REDIRECT_COUNT) + 1;
196:         $params->set(self::REDIRECT_COUNT, $current);
197: 
198:         // Use a provided maximum value or default to a max redirect count of 5
199:         $max = $params->hasKey(self::MAX_REDIRECTS)
200:             ? $params->get(self::MAX_REDIRECTS)
201:             : $this->defaultMaxRedirects;
202: 
203:         // Throw an exception if the redirect count is exceeded
204:         if ($current > $max) {
205:             return $this->throwTooManyRedirectsException($request);
206:         }
207: 
208:         return $original;
209:     }
210: 
211:     /**
212:      * Throw a too many redirects exception for a request
213:      *
214:      * @param RequestInterface $request Request
215:      * @throws TooManyRedirectsException when too many redirects have been issued
216:      */
217:     protected function throwTooManyRedirectsException(RequestInterface $request)
218:     {
219:         $lines = array();
220:         $response = $request->getResponse();
221: 
222:         do {
223:             $lines[] = '> ' . $response->getRequest()->getRawHeaders() . "\n\n< " . $response->getRawHeaders();
224:             $response = $response->getPreviousResponse();
225:         } while ($response);
226: 
227:         throw new TooManyRedirectsException(
228:             "Too many redirects were issued for this transaction:\n"
229:             . implode("* Sending redirect request\n", array_reverse($lines))
230:         );
231:     }
232: }
233: 
php-coveralls API documentation generated by ApiGen 2.8.0