diff --git a/classes/Kohana/Request/Client.php b/classes/Kohana/Request/Client.php index 8a3c4e792..579f916f6 100644 --- a/classes/Kohana/Request/Client.php +++ b/classes/Kohana/Request/Client.php @@ -1,4 +1,4 @@ - 'Request_Client::on_header_location' + ); + /** * Creates a new `Request_Client` object, * allows for dependency injection. @@ -74,54 +81,42 @@ public function __construct(array $params = array()) */ public function execute(Request $request) { - $response = Response::factory(); + $orig_response = $response = Response::factory(); if (($cache = $this->cache()) instanceof HTTP_Cache) return $cache->execute($this, $request, $response); $response = $this->execute_request($request, $response); - // Do we need to follow a Location header ? - if ($this->follow() AND in_array($response->status(), array(201, 301, 302, 303, 307)) - AND $response->headers('Location')) + // Execute response callbacks + foreach ($this->header_callbacks() as $header => $callback) { - // Figure out which method to use for the follow request - switch ($response->status()) + if ($response->headers($header)) { - default: - case 301: - case 307: - $follow_method = $request->method(); - break; - case 201: - case 303: - $follow_method = Request::GET; - break; - case 302: - // Cater for sites with broken HTTP redirect implementations - if ($this->strict_redirect()) - { - $follow_method = $request->method(); - } - else - { - $follow_method = Request::GET; - } - break; - } + $cb_result = call_user_func($callback, $request, $response, $this); - // Prepare the additional request - $follow_request = Request::factory($response->headers('Location')) - ->method($follow_method) - ->headers(Arr::extract($request->headers(), $this->follow_headers())); + if ($cb_result instanceof Request) + { + // If the callback returns a request, automatically assign client params + $client = $cb_result->client(); + $client->cache($this->cache()); + $client->follow($this->follow()); + $client->follow_headers($this->follow_headers()); + $client->header_callbacks($this->header_callbacks()); - if ($follow_method !== Request::GET) - { - $follow_request->body($request->body()); - } + // Execute the request + $response = $cb_result->execute(); + } + elseif ($cb_result instanceof Response) + { + // Assign the returned response + $response = $cb_result; + } - // Execute the additional request - $response = $follow_request->execute(); + // If the callback has created a new response, do not process any further + if ($response !== $orig_response) + break; + } } return $response; @@ -215,4 +210,91 @@ public function strict_redirect($strict_redirect = NULL) return $this; } + + /** + * Getter and setter for the header callbacks array. + * + * Accepts an array with HTTP response headers as keys and a PHP callback + * function as values. These callbacks will be triggered if a response contains + * the given header and can either issue a subsequent request or manipulate + * the response as required. + * + * By default, the [Request_Client::on_header_location] callback is assigned + * to the Location header to support automatic redirect following. + * + * $client->header_callbacks(array( + * 'Location' => 'Request_Client::on_header_location', + * 'WWW-Authenticate' => function($request, $response, $client) {return $new_response;}, + * ); + * + * @param array $header_callbacks Array of callbacks to trigger on presence of given headers + * @return Request_Client + */ + public function header_callbacks($header_callbacks = NULL) + { + if ($header_callbacks === NULL) + return $this->_header_callbacks; + + $this->_header_callbacks = $header_callbacks; + + return $this; + } + + /** + * The default handler for following redirects, triggered by the presence of + * a Location header in the response. + * + * The client's follow property must be set TRUE and the HTTP response status + * one of 201, 301, 302, 303 or 307 for the redirect to be followed. + * + * @param Request $request + * @param Response $response + * @param Request_Client $client + */ + public static function on_header_location(Request $request, Response $response, Request_Client $client) + { + // Do we need to follow a Location header ? + if ($client->follow() AND in_array($response->status(), array(201, 301, 302, 303, 307))) + { + // Figure out which method to use for the follow request + switch ($response->status()) + { + default: + case 301: + case 307: + $follow_method = $request->method(); + break; + case 201: + case 303: + $follow_method = Request::GET; + break; + case 302: + // Cater for sites with broken HTTP redirect implementations + if ($client->strict_redirect()) + { + $follow_method = $request->method(); + } + else + { + $follow_method = Request::GET; + } + break; + } + + // Prepare the additional request + $follow_request = Request::factory($response->headers('Location')) + ->method($follow_method) + ->headers(Arr::extract($request->headers(), $client->follow_headers())); + + if ($follow_method !== Request::GET) + { + $follow_request->body($request->body()); + } + + return $follow_request; + } + + return NULL; + } + } \ No newline at end of file diff --git a/tests/kohana/request/ClientTest.php b/tests/kohana/request/ClientTest.php index 9a7873d23..f3ee88e87 100644 --- a/tests/kohana/request/ClientTest.php +++ b/tests/kohana/request/ClientTest.php @@ -259,6 +259,110 @@ public function test_follows_with_body_if_not_get($original_method, $status, $ex $this->assertEquals($expect_body, $data['rq_body']); } + /** + * Provider for test_triggers_header_callbacks + * + * @return array + */ + public function provider_triggers_header_callbacks() + { + return array( + // Straightforward response manipulation + array( + array('X-test-1' => + function($request, $response, $client){ + $response->body(json_encode(array('body'=>'test1-body-changed'))); + return $response; + }), + $this->_dummy_uri(200, array('X-test-1' => 'foo'), 'test1-body'), + 'test1-body-changed' + ), + // Subsequent request execution + array( + array('X-test-2' => + function($request, $response, $client){ + return Request::factory($response->headers('X-test-2')); + }), + $this->_dummy_uri(200, + array('X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')), + 'test2-orig-body'), + 'test2-subsequent-body' + ), + // No callbacks triggered + array( + array('X-test-3' => + function ($request, $response, $client) { + throw new Exception("Unexpected execution of X-test-3 callback"); + }), + $this->_dummy_uri(200, array('X-test-1' => 'foo'), 'test3-body'), + 'test3-body' + ), + // Callbacks not triggered once a previous callback has created a new response + array( + array( + 'X-test-1' => + function($request, $response, $client){ + return Request::factory($response->headers('X-test-1')); + }, + 'X-test-2' => + function($request, $response, $client){ + return Request::factory($response->headers('X-test-2')); + } + ), + $this->_dummy_uri(200, + array( + 'X-test-1' => $this->_dummy_uri(200, NULL, 'test1-subsequent-body'), + 'X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body') + ), + 'test2-orig-body'), + 'test1-subsequent-body' + ), + // Nested callbacks are supported if callback creates new request + array( + array( + 'X-test-1' => + function($request, $response, $client){ + return Request::factory($response->headers('X-test-1')); + }, + 'X-test-2' => + function($request, $response, $client){ + return Request::factory($response->headers('X-test-2')); + } + ), + $this->_dummy_uri(200, + array( + 'X-test-1' => $this->_dummy_uri( + 200, + array('X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')), + 'test1-subsequent-body'), + ), + 'test-orig-body'), + 'test2-subsequent-body' + ), + ); + } + + /** + * Tests that header callbacks are triggered in sequence when specific headers + * are present in the response + * + * @dataProvider provider_triggers_header_callbacks + * + * @param array $callbacks Array of header callbacks + * @param array $headers Headers that will be received in the response + * @param string $expect_body Response body content to expect + */ + public function test_triggers_header_callbacks($callbacks, $uri, $expect_body) + { + $response = Request::factory($uri, + array('header_callbacks' => $callbacks)) + ->execute(); + + $data = json_decode($response->body(), TRUE); + + $this->assertEquals($expect_body, $data['body']); + } + } // End Kohana_Request_ClientTest