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 headers if (!$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 request if (!$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 is while ($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 URL if (!$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 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; } /** * 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 exceeded if ($current > $max) { $this->throwTooManyRedirectsException($original, $max); return false; } else { // Create a redirect request based on the redirect rules set on the request return $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 response if ($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); } ); } }