Merge pull request #247 from Sigill/post-redirect-get

Support post-redirect-get ("PRG") requests.
This commit is contained in:
Zach Borboa
2016-03-05 09:15:11 -08:00
7 changed files with 214 additions and 55 deletions
+22 -2
View File
@@ -53,8 +53,28 @@ $curl->post('http://www.example.com/login/', array(
'username' => 'myusername',
'password' => 'mypassword',
));
// Perform a post-redirect-get request (POST data and follow 303 redirections using GET requests).
$curl = new Curl();
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true);¬
$curl->post('http://www.example.com/login/', array(
'username' => 'myusername',
'password' => 'mypassword',
));
// POST data and follow 303 redirections by POSTing data again.
// Please note that 303 redirections should not be handled this way:
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
$curl = new Curl();
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true);¬
$curl->post('http://www.example.com/login/', array(
'username' => 'myusername',
'password' => 'mypassword',
), false);
```
A POST request performs by default a post-redirect-get (see above). Other request methods force an option which conflicts with the post-redirect-get behavior. Due to technical limitations of PHP engines <5.5.11 and HHVM, it is not possible to reset this option. It is therefore impossible to perform a post-redirect-get request using a php-curl-class Curl object that has already been used to perform other types of requests. Either use a new php-curl-class Curl object or upgrade your PHP engine.
```php
$curl = new Curl();
$curl->setBasicAuthentication('username', 'password');
@@ -192,7 +212,7 @@ Curl::head($url, $data = array())
Curl::headerCallback($ch, $header)
Curl::options($url, $data = array())
Curl::patch($url, $data = array())
Curl::post($url, $data = array())
Curl::post($url, $data = array(), $post_redirect_get = false)
Curl::progress($callback)
Curl::put($url, $data = array())
Curl::setBasicAuthentication($username, $password = '')
@@ -229,7 +249,7 @@ MultiCurl::addGet($url, $data = array())
MultiCurl::addHead($url, $data = array())
MultiCurl::addOptions($url, $data = array())
MultiCurl::addPatch($url, $data = array())
MultiCurl::addPost($url, $data = array())
MultiCurl::addPost($url, $data = array(), $post_redirect_get = false)
MultiCurl::addPut($url, $data = array())
MultiCurl::beforeSend($callback)
MultiCurl::close()
+35 -2
View File
@@ -505,18 +505,51 @@ class Curl
* @access public
* @param $url
* @param $data
* @param $follow_303_with_post If true, will cause 303 redirections to be followed using
* a POST request (default: false).
* Notes:
* - Redirections are only followed if the CURLOPT_FOLLOWLOCATION option is set to true.
* - According to the HTTP specs (see [1]), a 303 redirection should be followed using
* the GET method. 301 and 302 must not.
* - In order to force a 303 redirection to be performed using the same method, the
* underlying cURL object must be set in a special state (the CURLOPT_CURSTOMREQUEST
* option must be set to the method to use after the redirection). Due to a limitation
* of the cURL extension of PHP < 5.5.11 ([2], [3]) and of HHVM, it is not possible
* to reset this option. Using these PHP engines, it is therefore impossible to
* restore this behavior on an existing php-curl-class Curl object.
*
* @return string
*
* [1] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.2
* [2] https://github.com/php/php-src/pull/531
* [3] http://php.net/ChangeLog-5.php#5.5.11
*/
public function post($url, $data = array())
public function post($url, $data = array(), $follow_303_with_post = false)
{
if (is_array($url)) {
$follow_303_with_post = (bool)$data;
$data = $url;
$url = $this->baseUrl;
}
$this->setURL($url);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
if ($follow_303_with_post) {
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
} else {
if (isset($this->options[CURLOPT_CUSTOMREQUEST])) {
if ((version_compare(PHP_VERSION, '5.5.11') < 0) || defined('HHVM_VERSION')) {
trigger_error('Due to technical limitations of PHP <= 5.5.11 and HHVM, it is not possible to '
. 'perform a post-redirect-get request using a php-curl-class Curl object that '
. 'has already been used to perform other types of requests. Either use a new '
. 'php-curl-class Curl object or upgrade your PHP engine.',
E_USER_ERROR);
} else {
$this->setOpt(CURLOPT_CUSTOMREQUEST, null);
}
}
}
$this->setOpt(CURLOPT_POST, true);
$this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data));
return $this->exec();
+14 -2
View File
@@ -191,12 +191,16 @@ class MultiCurl
* @access public
* @param $url
* @param $data
* @param $post_redirect_get If true, will cause 303 redirections to be followed using
* GET requests (default: false).
* Note: Redirections are only followed if the CURLOPT_FOLLOWLOCATION option is set to true.
*
* @return object
*/
public function addPost($url, $data = array())
public function addPost($url, $data = array(), $post_redirect_get = false)
{
if (is_array($url)) {
$post_redirect_get = (bool)$data;
$data = $url;
$url = $this->baseUrl;
}
@@ -208,7 +212,15 @@ class MultiCurl
}
$curl->setURL($url);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
/*
* For post-redirect-get requests, the CURLOPT_CUSTOMREQUEST option must not
* be set, otherwise cURL will perform POST requests for redirections.
*/
if (!$post_redirect_get) {
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
}
$curl->setOpt(CURLOPT_POST, true);
$curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data));
$this->addHandle($curl);
+20 -7
View File
@@ -26,14 +26,27 @@ class Test
}
return $this->curl->response;
}
}
function test($instance, $before, $after)
{
$instance->server('request_method', $before);
\PHPUnit_Framework_Assert::assertEquals($before, $instance->curl->responseHeaders['X-REQUEST-METHOD']);
$instance->server('request_method', $after);
\PHPUnit_Framework_Assert::assertEquals($after, $instance->curl->responseHeaders['X-REQUEST-METHOD']);
/*
* When chaining requests, the method must be forced, otherwise a
* previously forced method might be inherited.
* Especially, POSTs must be configured to not perform post-redirect-get.
*/
private function chained_request($request_method)
{
if ($request_method === 'POST') {
$this->server('request_method', $request_method, array(), true);
} else {
$this->server('request_method', $request_method);
}
\PHPUnit_Framework_Assert::assertEquals($request_method, $this->curl->responseHeaders['X-REQUEST-METHOD']);
}
public function chain_requests($first, $second)
{
$this->chained_request($first);
$this->chained_request($second);
}
}
function create_png()
+84 -42
View File
@@ -408,6 +408,47 @@ class CurlTest extends PHPUnit_Framework_TestCase
$this->assertEquals('foo=bar&file=%40not-a-file', $test->curl->response);
}
public function testPostRedirectGet()
{
// Follow 303 redirection with GET
$test = new Test();
$test->curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
$this->assertEquals('Redirected: GET', $test->server('post_redirect_get', 'POST'));
// Follow 303 redirection with POST
$test = new Test();
$test->curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
$this->assertEquals('Redirected: POST', $test->server('post_redirect_get', 'POST', array(), true));
// On compatible PHP engines, ensure that it is possible to reuse an existing Curl object
if ((version_compare(PHP_VERSION, '5.5.11') > 0) && !defined('HHVM_VERSION')) {
$this->assertEquals('Redirected: GET', $test->server('post_redirect_get', 'POST'));
}
}
public function testPostRedirectGetReuseObjectIncompatibleEngine()
{
if ((version_compare(PHP_VERSION, '5.5.11') > 0) && !defined('HHVM_VERSION')) {
$this->markTestSkipped('This test is not applicable to this platform.');
}
try {
// Follow 303 redirection with POST
$test = new Test();
$test->curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
$test->server('post_redirect_get', 'POST', array(), true);
// On incompatible PHP engines, reusing an existing Curl object to perform a
// post-redirect-get request will trigger a PHP error
$test->server('post_redirect_get', 'POST');
$this->assertTrue(false,
'Reusing an existing Curl object on incompatible PHP engines shall trigger an error.');
} catch (PHPUnit_Framework_Error $e) {
$this->assertTrue(true);
}
}
public function testPutRequestMethod()
{
$test = new Test();
@@ -538,6 +579,7 @@ class CurlTest extends PHPUnit_Framework_TestCase
$this->assertFalse(is_bool($download_test->curl->rawResponse));
// Remove server file.
$download_test = new Test();
$this->assertEquals('true', $download_test->server('upload_cleanup', 'POST', array(
'file_path' => $uploaded_file_path,
)));
@@ -2457,78 +2499,78 @@ class CurlTest extends PHPUnit_Framework_TestCase
public function testRequestMethodSuccessiveGetRequests()
{
$test = new Test();
Helper\test($test, 'GET', 'POST');
Helper\test($test, 'GET', 'PUT');
Helper\test($test, 'GET', 'PATCH');
Helper\test($test, 'GET', 'DELETE');
Helper\test($test, 'GET', 'HEAD');
Helper\test($test, 'GET', 'OPTIONS');
$test->chain_requests('GET', 'POST');
$test->chain_requests('GET', 'PUT');
$test->chain_requests('GET', 'PATCH');
$test->chain_requests('GET', 'DELETE');
$test->chain_requests('GET', 'HEAD');
$test->chain_requests('GET', 'OPTIONS');
}
public function testRequestMethodSuccessivePostRequests()
{
$test = new Test();
Helper\test($test, 'POST', 'GET');
Helper\test($test, 'POST', 'PUT');
Helper\test($test, 'POST', 'PATCH');
Helper\test($test, 'POST', 'DELETE');
Helper\test($test, 'POST', 'HEAD');
Helper\test($test, 'POST', 'OPTIONS');
$test->chain_requests('POST', 'GET');
$test->chain_requests('POST', 'PUT');
$test->chain_requests('POST', 'PATCH');
$test->chain_requests('POST', 'DELETE');
$test->chain_requests('POST', 'HEAD');
$test->chain_requests('POST', 'OPTIONS');
}
public function testRequestMethodSuccessivePutRequests()
{
$test = new Test();
Helper\test($test, 'PUT', 'GET');
Helper\test($test, 'PUT', 'POST');
Helper\test($test, 'PUT', 'PATCH');
Helper\test($test, 'PUT', 'DELETE');
Helper\test($test, 'PUT', 'HEAD');
Helper\test($test, 'PUT', 'OPTIONS');
$test->chain_requests('PUT', 'GET');
$test->chain_requests('PUT', 'POST');
$test->chain_requests('PUT', 'PATCH');
$test->chain_requests('PUT', 'DELETE');
$test->chain_requests('PUT', 'HEAD');
$test->chain_requests('PUT', 'OPTIONS');
}
public function testRequestMethodSuccessivePatchRequests()
{
$test = new Test();
Helper\test($test, 'PATCH', 'GET');
Helper\test($test, 'PATCH', 'POST');
Helper\test($test, 'PATCH', 'PUT');
Helper\test($test, 'PATCH', 'DELETE');
Helper\test($test, 'PATCH', 'HEAD');
Helper\test($test, 'PATCH', 'OPTIONS');
$test->chain_requests('PATCH', 'GET');
$test->chain_requests('PATCH', 'POST');
$test->chain_requests('PATCH', 'PUT');
$test->chain_requests('PATCH', 'DELETE');
$test->chain_requests('PATCH', 'HEAD');
$test->chain_requests('PATCH', 'OPTIONS');
}
public function testRequestMethodSuccessiveDeleteRequests()
{
$test = new Test();
Helper\test($test, 'DELETE', 'GET');
Helper\test($test, 'DELETE', 'POST');
Helper\test($test, 'DELETE', 'PUT');
Helper\test($test, 'DELETE', 'PATCH');
Helper\test($test, 'DELETE', 'HEAD');
Helper\test($test, 'DELETE', 'OPTIONS');
$test->chain_requests('DELETE', 'GET');
$test->chain_requests('DELETE', 'POST');
$test->chain_requests('DELETE', 'PUT');
$test->chain_requests('DELETE', 'PATCH');
$test->chain_requests('DELETE', 'HEAD');
$test->chain_requests('DELETE', 'OPTIONS');
}
public function testRequestMethodSuccessiveHeadRequests()
{
$test = new Test();
Helper\test($test, 'HEAD', 'GET');
Helper\test($test, 'HEAD', 'POST');
Helper\test($test, 'HEAD', 'PUT');
Helper\test($test, 'HEAD', 'PATCH');
Helper\test($test, 'HEAD', 'DELETE');
Helper\test($test, 'HEAD', 'OPTIONS');
$test->chain_requests('HEAD', 'GET');
$test->chain_requests('HEAD', 'POST');
$test->chain_requests('HEAD', 'PUT');
$test->chain_requests('HEAD', 'PATCH');
$test->chain_requests('HEAD', 'DELETE');
$test->chain_requests('HEAD', 'OPTIONS');
}
public function testRequestMethodSuccessiveOptionsRequests()
{
$test = new Test();
Helper\test($test, 'OPTIONS', 'GET');
Helper\test($test, 'OPTIONS', 'POST');
Helper\test($test, 'OPTIONS', 'PUT');
Helper\test($test, 'OPTIONS', 'PATCH');
Helper\test($test, 'OPTIONS', 'DELETE');
Helper\test($test, 'OPTIONS', 'HEAD');
$test->chain_requests('OPTIONS', 'GET');
$test->chain_requests('OPTIONS', 'POST');
$test->chain_requests('OPTIONS', 'PUT');
$test->chain_requests('OPTIONS', 'PATCH');
$test->chain_requests('OPTIONS', 'DELETE');
$test->chain_requests('OPTIONS', 'HEAD');
}
public function testMemoryLeak()
@@ -2224,4 +2224,25 @@ class MultiCurlTest extends PHPUnit_Framework_TestCase
$multi_curl->close();
$this->assertFalse(is_resource($multi_curl->multiCurl));
}
public function testMultiPostRedirectGet()
{
// Deny post-redirect-get
$multi_curl = new MultiCurl(Test::TEST_URL);
$multi_curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
$multi_curl->setHeader('X-DEBUG-TEST', 'post_redirect_get');
$multi_curl->addPost(array(), false)->complete(function($instance) {
PHPUnit_Framework_Assert::assertEquals('Redirected: POST', $instance->response);
});
$multi_curl->start();
// Allow post-redirect-get
$multi_curl = new MultiCurl(Test::TEST_URL);
$multi_curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
$multi_curl->setHeader('X-DEBUG-TEST', 'post_redirect_get');
$multi_curl->addPost(array(), true)->complete(function($instance) {
PHPUnit_Framework_Assert::assertEquals('Redirected: GET', $instance->response);
});
$multi_curl->start();
}
}
+18
View File
@@ -264,6 +264,24 @@ if ($test === 'http_basic_auth') {
} elseif ($test === 'data_values') {
header('Content-Type: application/json');
echo json_encode($data_values);
exit;
} elseif ($test === 'post_redirect_get') {
if (isset($_GET['redirect'])) {
echo "Redirected: $request_method";
} else {
if ($request_method === 'POST') {
if (function_exists('http_response_code')) {
http_response_code(303);
} else {
header('HTTP/1.1 303 See Other');
}
header('Location: ?redirect');
} else {
echo "Request method is $request_method, but POST was expected";
}
}
exit;
}