Skip to content

Commit 3276037

Browse files
committed
Merge pull request php-curl-class#247 from Sigill/post-redirect-get
Support post-redirect-get ("PRG") requests.
2 parents b7143c0 + dc60016 commit 3276037

File tree

7 files changed

+214
-55
lines changed

7 files changed

+214
-55
lines changed

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,28 @@ $curl->post('http://www.example.com/login/', array(
5353
'username' => 'myusername',
5454
'password' => 'mypassword',
5555
));
56+
57+
// Perform a post-redirect-get request (POST data and follow 303 redirections using GET requests).
58+
$curl = new Curl();
59+
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true);¬
60+
$curl->post('http://www.example.com/login/', array(
61+
'username' => 'myusername',
62+
'password' => 'mypassword',
63+
));
64+
65+
// POST data and follow 303 redirections by POSTing data again.
66+
// Please note that 303 redirections should not be handled this way:
67+
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
68+
$curl = new Curl();
69+
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true);¬
70+
$curl->post('http://www.example.com/login/', array(
71+
'username' => 'myusername',
72+
'password' => 'mypassword',
73+
), false);
5674
```
5775

76+
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.
77+
5878
```php
5979
$curl = new Curl();
6080
$curl->setBasicAuthentication('username', 'password');
@@ -192,7 +212,7 @@ Curl::head($url, $data = array())
192212
Curl::headerCallback($ch, $header)
193213
Curl::options($url, $data = array())
194214
Curl::patch($url, $data = array())
195-
Curl::post($url, $data = array())
215+
Curl::post($url, $data = array(), $post_redirect_get = false)
196216
Curl::progress($callback)
197217
Curl::put($url, $data = array())
198218
Curl::setBasicAuthentication($username, $password = '')
@@ -229,7 +249,7 @@ MultiCurl::addGet($url, $data = array())
229249
MultiCurl::addHead($url, $data = array())
230250
MultiCurl::addOptions($url, $data = array())
231251
MultiCurl::addPatch($url, $data = array())
232-
MultiCurl::addPost($url, $data = array())
252+
MultiCurl::addPost($url, $data = array(), $post_redirect_get = false)
233253
MultiCurl::addPut($url, $data = array())
234254
MultiCurl::beforeSend($callback)
235255
MultiCurl::close()

src/Curl/Curl.php

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -505,18 +505,51 @@ public function patch($url, $data = array())
505505
* @access public
506506
* @param $url
507507
* @param $data
508+
* @param $follow_303_with_post If true, will cause 303 redirections to be followed using
509+
* a POST request (default: false).
510+
* Notes:
511+
* - Redirections are only followed if the CURLOPT_FOLLOWLOCATION option is set to true.
512+
* - According to the HTTP specs (see [1]), a 303 redirection should be followed using
513+
* the GET method. 301 and 302 must not.
514+
* - In order to force a 303 redirection to be performed using the same method, the
515+
* underlying cURL object must be set in a special state (the CURLOPT_CURSTOMREQUEST
516+
* option must be set to the method to use after the redirection). Due to a limitation
517+
* of the cURL extension of PHP < 5.5.11 ([2], [3]) and of HHVM, it is not possible
518+
* to reset this option. Using these PHP engines, it is therefore impossible to
519+
* restore this behavior on an existing php-curl-class Curl object.
508520
*
509521
* @return string
522+
*
523+
* [1] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.2
524+
* [2] https://github.com/php/php-src/pull/531
525+
* [3] http://php.net/ChangeLog-5.php#5.5.11
510526
*/
511-
public function post($url, $data = array())
527+
public function post($url, $data = array(), $follow_303_with_post = false)
512528
{
513529
if (is_array($url)) {
530+
$follow_303_with_post = (bool)$data;
514531
$data = $url;
515532
$url = $this->baseUrl;
516533
}
517534

518535
$this->setURL($url);
519-
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
536+
537+
if ($follow_303_with_post) {
538+
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
539+
} else {
540+
if (isset($this->options[CURLOPT_CUSTOMREQUEST])) {
541+
if ((version_compare(PHP_VERSION, '5.5.11') < 0) || defined('HHVM_VERSION')) {
542+
trigger_error('Due to technical limitations of PHP <= 5.5.11 and HHVM, it is not possible to '
543+
. 'perform a post-redirect-get request using a php-curl-class Curl object that '
544+
. 'has already been used to perform other types of requests. Either use a new '
545+
. 'php-curl-class Curl object or upgrade your PHP engine.',
546+
E_USER_ERROR);
547+
} else {
548+
$this->setOpt(CURLOPT_CUSTOMREQUEST, null);
549+
}
550+
}
551+
}
552+
520553
$this->setOpt(CURLOPT_POST, true);
521554
$this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data));
522555
return $this->exec();

src/Curl/MultiCurl.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,16 @@ public function addPatch($url, $data = array())
191191
* @access public
192192
* @param $url
193193
* @param $data
194+
* @param $post_redirect_get If true, will cause 303 redirections to be followed using
195+
* GET requests (default: false).
196+
* Note: Redirections are only followed if the CURLOPT_FOLLOWLOCATION option is set to true.
194197
*
195198
* @return object
196199
*/
197-
public function addPost($url, $data = array())
200+
public function addPost($url, $data = array(), $post_redirect_get = false)
198201
{
199202
if (is_array($url)) {
203+
$post_redirect_get = (bool)$data;
200204
$data = $url;
201205
$url = $this->baseUrl;
202206
}
@@ -208,7 +212,15 @@ public function addPost($url, $data = array())
208212
}
209213

210214
$curl->setURL($url);
211-
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
215+
216+
/*
217+
* For post-redirect-get requests, the CURLOPT_CUSTOMREQUEST option must not
218+
* be set, otherwise cURL will perform POST requests for redirections.
219+
*/
220+
if (!$post_redirect_get) {
221+
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
222+
}
223+
212224
$curl->setOpt(CURLOPT_POST, true);
213225
$curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data));
214226
$this->addHandle($curl);

tests/PHPCurlClass/Helper.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,27 @@ public function server($test, $request_method, $query_parameters = array(), $dat
2626
}
2727
return $this->curl->response;
2828
}
29-
}
3029

31-
function test($instance, $before, $after)
32-
{
33-
$instance->server('request_method', $before);
34-
\PHPUnit_Framework_Assert::assertEquals($before, $instance->curl->responseHeaders['X-REQUEST-METHOD']);
35-
$instance->server('request_method', $after);
36-
\PHPUnit_Framework_Assert::assertEquals($after, $instance->curl->responseHeaders['X-REQUEST-METHOD']);
30+
/*
31+
* When chaining requests, the method must be forced, otherwise a
32+
* previously forced method might be inherited.
33+
* Especially, POSTs must be configured to not perform post-redirect-get.
34+
*/
35+
private function chained_request($request_method)
36+
{
37+
if ($request_method === 'POST') {
38+
$this->server('request_method', $request_method, array(), true);
39+
} else {
40+
$this->server('request_method', $request_method);
41+
}
42+
\PHPUnit_Framework_Assert::assertEquals($request_method, $this->curl->responseHeaders['X-REQUEST-METHOD']);
43+
}
44+
45+
public function chain_requests($first, $second)
46+
{
47+
$this->chained_request($first);
48+
$this->chained_request($second);
49+
}
3750
}
3851

3952
function create_png()

tests/PHPCurlClass/PHPCurlClassTest.php

Lines changed: 84 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,47 @@ public function testPostNonFilePathUpload()
408408
$this->assertEquals('foo=bar&file=%40not-a-file', $test->curl->response);
409409
}
410410

411+
public function testPostRedirectGet()
412+
{
413+
// Follow 303 redirection with GET
414+
$test = new Test();
415+
$test->curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
416+
$this->assertEquals('Redirected: GET', $test->server('post_redirect_get', 'POST'));
417+
418+
// Follow 303 redirection with POST
419+
$test = new Test();
420+
$test->curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
421+
$this->assertEquals('Redirected: POST', $test->server('post_redirect_get', 'POST', array(), true));
422+
423+
// On compatible PHP engines, ensure that it is possible to reuse an existing Curl object
424+
if ((version_compare(PHP_VERSION, '5.5.11') > 0) && !defined('HHVM_VERSION')) {
425+
$this->assertEquals('Redirected: GET', $test->server('post_redirect_get', 'POST'));
426+
}
427+
}
428+
429+
public function testPostRedirectGetReuseObjectIncompatibleEngine()
430+
{
431+
if ((version_compare(PHP_VERSION, '5.5.11') > 0) && !defined('HHVM_VERSION')) {
432+
$this->markTestSkipped('This test is not applicable to this platform.');
433+
}
434+
435+
try {
436+
// Follow 303 redirection with POST
437+
$test = new Test();
438+
$test->curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
439+
$test->server('post_redirect_get', 'POST', array(), true);
440+
441+
// On incompatible PHP engines, reusing an existing Curl object to perform a
442+
// post-redirect-get request will trigger a PHP error
443+
$test->server('post_redirect_get', 'POST');
444+
445+
$this->assertTrue(false,
446+
'Reusing an existing Curl object on incompatible PHP engines shall trigger an error.');
447+
} catch (PHPUnit_Framework_Error $e) {
448+
$this->assertTrue(true);
449+
}
450+
}
451+
411452
public function testPutRequestMethod()
412453
{
413454
$test = new Test();
@@ -538,6 +579,7 @@ public function testDownload()
538579
$this->assertFalse(is_bool($download_test->curl->rawResponse));
539580

540581
// Remove server file.
582+
$download_test = new Test();
541583
$this->assertEquals('true', $download_test->server('upload_cleanup', 'POST', array(
542584
'file_path' => $uploaded_file_path,
543585
)));
@@ -2457,78 +2499,78 @@ public function testRequiredOptionCurlOptReturnTransferEmitsWarning()
24572499
public function testRequestMethodSuccessiveGetRequests()
24582500
{
24592501
$test = new Test();
2460-
Helper\test($test, 'GET', 'POST');
2461-
Helper\test($test, 'GET', 'PUT');
2462-
Helper\test($test, 'GET', 'PATCH');
2463-
Helper\test($test, 'GET', 'DELETE');
2464-
Helper\test($test, 'GET', 'HEAD');
2465-
Helper\test($test, 'GET', 'OPTIONS');
2502+
$test->chain_requests('GET', 'POST');
2503+
$test->chain_requests('GET', 'PUT');
2504+
$test->chain_requests('GET', 'PATCH');
2505+
$test->chain_requests('GET', 'DELETE');
2506+
$test->chain_requests('GET', 'HEAD');
2507+
$test->chain_requests('GET', 'OPTIONS');
24662508
}
24672509

24682510
public function testRequestMethodSuccessivePostRequests()
24692511
{
24702512
$test = new Test();
2471-
Helper\test($test, 'POST', 'GET');
2472-
Helper\test($test, 'POST', 'PUT');
2473-
Helper\test($test, 'POST', 'PATCH');
2474-
Helper\test($test, 'POST', 'DELETE');
2475-
Helper\test($test, 'POST', 'HEAD');
2476-
Helper\test($test, 'POST', 'OPTIONS');
2513+
$test->chain_requests('POST', 'GET');
2514+
$test->chain_requests('POST', 'PUT');
2515+
$test->chain_requests('POST', 'PATCH');
2516+
$test->chain_requests('POST', 'DELETE');
2517+
$test->chain_requests('POST', 'HEAD');
2518+
$test->chain_requests('POST', 'OPTIONS');
24772519
}
24782520

24792521
public function testRequestMethodSuccessivePutRequests()
24802522
{
24812523
$test = new Test();
2482-
Helper\test($test, 'PUT', 'GET');
2483-
Helper\test($test, 'PUT', 'POST');
2484-
Helper\test($test, 'PUT', 'PATCH');
2485-
Helper\test($test, 'PUT', 'DELETE');
2486-
Helper\test($test, 'PUT', 'HEAD');
2487-
Helper\test($test, 'PUT', 'OPTIONS');
2524+
$test->chain_requests('PUT', 'GET');
2525+
$test->chain_requests('PUT', 'POST');
2526+
$test->chain_requests('PUT', 'PATCH');
2527+
$test->chain_requests('PUT', 'DELETE');
2528+
$test->chain_requests('PUT', 'HEAD');
2529+
$test->chain_requests('PUT', 'OPTIONS');
24882530
}
24892531

24902532
public function testRequestMethodSuccessivePatchRequests()
24912533
{
24922534
$test = new Test();
2493-
Helper\test($test, 'PATCH', 'GET');
2494-
Helper\test($test, 'PATCH', 'POST');
2495-
Helper\test($test, 'PATCH', 'PUT');
2496-
Helper\test($test, 'PATCH', 'DELETE');
2497-
Helper\test($test, 'PATCH', 'HEAD');
2498-
Helper\test($test, 'PATCH', 'OPTIONS');
2535+
$test->chain_requests('PATCH', 'GET');
2536+
$test->chain_requests('PATCH', 'POST');
2537+
$test->chain_requests('PATCH', 'PUT');
2538+
$test->chain_requests('PATCH', 'DELETE');
2539+
$test->chain_requests('PATCH', 'HEAD');
2540+
$test->chain_requests('PATCH', 'OPTIONS');
24992541
}
25002542

25012543
public function testRequestMethodSuccessiveDeleteRequests()
25022544
{
25032545
$test = new Test();
2504-
Helper\test($test, 'DELETE', 'GET');
2505-
Helper\test($test, 'DELETE', 'POST');
2506-
Helper\test($test, 'DELETE', 'PUT');
2507-
Helper\test($test, 'DELETE', 'PATCH');
2508-
Helper\test($test, 'DELETE', 'HEAD');
2509-
Helper\test($test, 'DELETE', 'OPTIONS');
2546+
$test->chain_requests('DELETE', 'GET');
2547+
$test->chain_requests('DELETE', 'POST');
2548+
$test->chain_requests('DELETE', 'PUT');
2549+
$test->chain_requests('DELETE', 'PATCH');
2550+
$test->chain_requests('DELETE', 'HEAD');
2551+
$test->chain_requests('DELETE', 'OPTIONS');
25102552
}
25112553

25122554
public function testRequestMethodSuccessiveHeadRequests()
25132555
{
25142556
$test = new Test();
2515-
Helper\test($test, 'HEAD', 'GET');
2516-
Helper\test($test, 'HEAD', 'POST');
2517-
Helper\test($test, 'HEAD', 'PUT');
2518-
Helper\test($test, 'HEAD', 'PATCH');
2519-
Helper\test($test, 'HEAD', 'DELETE');
2520-
Helper\test($test, 'HEAD', 'OPTIONS');
2557+
$test->chain_requests('HEAD', 'GET');
2558+
$test->chain_requests('HEAD', 'POST');
2559+
$test->chain_requests('HEAD', 'PUT');
2560+
$test->chain_requests('HEAD', 'PATCH');
2561+
$test->chain_requests('HEAD', 'DELETE');
2562+
$test->chain_requests('HEAD', 'OPTIONS');
25212563
}
25222564

25232565
public function testRequestMethodSuccessiveOptionsRequests()
25242566
{
25252567
$test = new Test();
2526-
Helper\test($test, 'OPTIONS', 'GET');
2527-
Helper\test($test, 'OPTIONS', 'POST');
2528-
Helper\test($test, 'OPTIONS', 'PUT');
2529-
Helper\test($test, 'OPTIONS', 'PATCH');
2530-
Helper\test($test, 'OPTIONS', 'DELETE');
2531-
Helper\test($test, 'OPTIONS', 'HEAD');
2568+
$test->chain_requests('OPTIONS', 'GET');
2569+
$test->chain_requests('OPTIONS', 'POST');
2570+
$test->chain_requests('OPTIONS', 'PUT');
2571+
$test->chain_requests('OPTIONS', 'PATCH');
2572+
$test->chain_requests('OPTIONS', 'DELETE');
2573+
$test->chain_requests('OPTIONS', 'HEAD');
25322574
}
25332575

25342576
public function testMemoryLeak()

tests/PHPCurlClass/PHPMultiCurlClassTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2224,4 +2224,25 @@ public function testClose()
22242224
$multi_curl->close();
22252225
$this->assertFalse(is_resource($multi_curl->multiCurl));
22262226
}
2227+
2228+
public function testMultiPostRedirectGet()
2229+
{
2230+
// Deny post-redirect-get
2231+
$multi_curl = new MultiCurl(Test::TEST_URL);
2232+
$multi_curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
2233+
$multi_curl->setHeader('X-DEBUG-TEST', 'post_redirect_get');
2234+
$multi_curl->addPost(array(), false)->complete(function($instance) {
2235+
PHPUnit_Framework_Assert::assertEquals('Redirected: POST', $instance->response);
2236+
});
2237+
$multi_curl->start();
2238+
2239+
// Allow post-redirect-get
2240+
$multi_curl = new MultiCurl(Test::TEST_URL);
2241+
$multi_curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
2242+
$multi_curl->setHeader('X-DEBUG-TEST', 'post_redirect_get');
2243+
$multi_curl->addPost(array(), true)->complete(function($instance) {
2244+
PHPUnit_Framework_Assert::assertEquals('Redirected: GET', $instance->response);
2245+
});
2246+
$multi_curl->start();
2247+
}
22272248
}

tests/PHPCurlClass/server.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,24 @@
264264
} elseif ($test === 'data_values') {
265265
header('Content-Type: application/json');
266266
echo json_encode($data_values);
267+
exit;
268+
} elseif ($test === 'post_redirect_get') {
269+
if (isset($_GET['redirect'])) {
270+
echo "Redirected: $request_method";
271+
} else {
272+
if ($request_method === 'POST') {
273+
if (function_exists('http_response_code')) {
274+
http_response_code(303);
275+
} else {
276+
header('HTTP/1.1 303 See Other');
277+
}
278+
279+
header('Location: ?redirect');
280+
} else {
281+
echo "Request method is $request_method, but POST was expected";
282+
}
283+
}
284+
267285
exit;
268286
}
269287

0 commit comments

Comments
 (0)