Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ PHP NEWS
- BCMath:
. Added NUL-byte validation to BCMath functions. (jorgsowa)

- Curl:
. Added CURLOPT_SEEKFUNCTION and the CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL
and CURL_SEEKFUNC_CANTSEEK constants, letting libcurl rewind a streamed
request body to resend it on a redirect, multi-pass authentication or a
retried reused connection. (GrahamCampbell)

- Date:
. Update timelib to 2022.16. (Derick)

Expand Down
14 changes: 14 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ PHP 8.6 UPGRADE NOTES
. It is now possible to define the `__debugInfo()` magic method on enums.
RFC: https://wiki.php.net/rfc/debugable-enums

- Curl:
. Added CURLOPT_SEEKFUNCTION to register a callback that repositions a
streamed request body so libcurl can rewind and resend it on a redirect,
multi-pass authentication, or a retried reused connection instead of
failing with CURLE_SEND_FAIL_REWIND. The callback receives the CurlHandle,
offset and origin, and must return one of CURL_SEEKFUNC_OK,
CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK.

- Fileinfo:
. finfo_file() now works with remote streams.

Expand Down Expand Up @@ -367,6 +375,12 @@ PHP 8.6 UPGRADE NOTES
10. New Global Constants
========================================

- Curl:
. CURLOPT_SEEKFUNCTION.
. CURL_SEEKFUNC_OK.
. CURL_SEEKFUNC_FAIL.
. CURL_SEEKFUNC_CANTSEEK.

- Sockets:
. TCP_USER_TIMEOUT (Linux only).
. AF_UNSPEC.
Expand Down
20 changes: 20 additions & 0 deletions ext/curl/curl.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,11 @@
* @cvalue CURLOPT_RETURNTRANSFER
*/
const CURLOPT_RETURNTRANSFER = UNKNOWN;
/**
* @var int
* @cvalue CURLOPT_SEEKFUNCTION
*/
const CURLOPT_SEEKFUNCTION = UNKNOWN;
/**
* @var int
* @cvalue CURLOPT_SHARE
Expand Down Expand Up @@ -1788,6 +1793,21 @@
* @cvalue CURL_READFUNC_PAUSE
*/
const CURL_READFUNC_PAUSE = UNKNOWN;
/**
* @var int
* @cvalue CURL_SEEKFUNC_OK
*/
const CURL_SEEKFUNC_OK = UNKNOWN;
/**
* @var int
* @cvalue CURL_SEEKFUNC_FAIL
*/
const CURL_SEEKFUNC_FAIL = UNKNOWN;
/**
* @var int
* @cvalue CURL_SEEKFUNC_CANTSEEK
*/
const CURL_SEEKFUNC_CANTSEEK = UNKNOWN;
/**
* @var int
* @cvalue CURL_WRITEFUNC_PAUSE
Expand Down
6 changes: 5 additions & 1 deletion ext/curl/curl_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ext/curl/curl_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ typedef struct {
php_curl_write *write_header;
php_curl_read *read;
zval std_err;
zend_fcall_info_cache seek;
zend_fcall_info_cache progress;
zend_fcall_info_cache xferinfo;
zend_fcall_info_cache fnmatch;
Expand Down
60 changes: 60 additions & 0 deletions ext/curl/interface.c
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,10 @@ static HashTable *curl_get_gc(zend_object *object, zval **table, int *n)
zend_get_gc_buffer_add_zval(gc_buffer, &curl->handlers.write_header->stream);
}

if (ZEND_FCC_INITIALIZED(curl->handlers.seek)) {
zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.seek);
}

if (ZEND_FCC_INITIALIZED(curl->handlers.progress)) {
zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.progress);
}
Expand Down Expand Up @@ -831,6 +835,52 @@ static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx)
}
/* }}} */

/* {{{ curl_seek */
static int curl_seek(void *clientp, curl_off_t offset, int origin)
{
php_curl *ch = (php_curl *)clientp;
int rval = CURL_SEEKFUNC_CANTSEEK; /* safe default if unset or the callback misbehaves */

#if PHP_CURL_DEBUG
fprintf(stderr, "curl_seek() called\n");
fprintf(stderr, "clientp = %x, offset = %ld, origin = %d\n", clientp, offset, origin);
#endif
if (!ZEND_FCC_INITIALIZED(ch->handlers.seek)) {
return rval;
}

zval args[3];
zval retval;

GC_ADDREF(&ch->std);
ZVAL_OBJ(&args[0], &ch->std);
ZVAL_LONG(&args[1], offset);
ZVAL_LONG(&args[2], origin);

ch->in_callback = true;
zend_call_known_fcc(&ch->handlers.seek, &retval, /* param_count */ 3, args, /* named_params */ NULL);
ch->in_callback = false;

if (!Z_ISUNDEF(retval)) {
_php_curl_verify_handlers(ch, /* reporterror */ true);
if (Z_TYPE(retval) == IS_LONG) {
zend_long retval_long = Z_LVAL(retval);
if (retval_long == CURL_SEEKFUNC_OK || retval_long == CURL_SEEKFUNC_FAIL || retval_long == CURL_SEEKFUNC_CANTSEEK) {
rval = retval_long;
} else {
zend_value_error("The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK");
}
} else {
zend_type_error("The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK");
}
zval_ptr_dtor(&retval);
}

zval_ptr_dtor(&args[0]);
return rval;
}
/* }}} */

/* {{{ curl_write_header */
static size_t curl_write_header(char *data, size_t size, size_t nmemb, void *ctx)
{
Expand Down Expand Up @@ -1038,6 +1088,7 @@ void init_curl_handle(php_curl *ch)
ch->handlers.write = ecalloc(1, sizeof(php_curl_write));
ch->handlers.write_header = ecalloc(1, sizeof(php_curl_write));
ch->handlers.read = ecalloc(1, sizeof(php_curl_read));
ch->handlers.seek = empty_fcall_info_cache;
ch->handlers.progress = empty_fcall_info_cache;
ch->handlers.xferinfo = empty_fcall_info_cache;
ch->handlers.fnmatch = empty_fcall_info_cache;
Expand Down Expand Up @@ -1208,6 +1259,7 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source)
curl_easy_setopt(ch->cp, CURLOPT_WRITEHEADER, (void *) ch);
curl_easy_setopt(ch->cp, CURLOPT_DEBUGDATA, (void *) ch);

php_curl_copy_fcc_with_option(ch, CURLOPT_SEEKDATA, &ch->handlers.seek, &source->handlers.seek);
php_curl_copy_fcc_with_option(ch, CURLOPT_PROGRESSDATA, &ch->handlers.progress, &source->handlers.progress);
php_curl_copy_fcc_with_option(ch, CURLOPT_XFERINFODATA, &ch->handlers.xferinfo, &source->handlers.xferinfo);
php_curl_copy_fcc_with_option(ch, CURLOPT_FNMATCH_DATA, &ch->handlers.fnmatch, &source->handlers.fnmatch);
Expand Down Expand Up @@ -1577,6 +1629,7 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue
HANDLE_CURL_OPTION_CALLABLE_PHP_CURL_USER(ch, CURLOPT_HEADER, write_header, PHP_CURL_IGNORE);
HANDLE_CURL_OPTION_CALLABLE_PHP_CURL_USER(ch, CURLOPT_READ, read, PHP_CURL_DIRECT);

HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_SEEK, handlers.seek, curl_seek);
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_PROGRESS, handlers.progress, curl_progress);
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_XFERINFO, handlers.xferinfo, curl_xferinfo);
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_FNMATCH_, handlers.fnmatch, curl_fnmatch);
Expand Down Expand Up @@ -2781,6 +2834,9 @@ static void curl_free_obj(zend_object *object)
efree(ch->handlers.write_header);
efree(ch->handlers.read);

if (ZEND_FCC_INITIALIZED(ch->handlers.seek)) {
zend_fcc_dtor(&ch->handlers.seek);
}
if (ZEND_FCC_INITIALIZED(ch->handlers.progress)) {
zend_fcc_dtor(&ch->handlers.progress);
}
Expand Down Expand Up @@ -2865,6 +2921,10 @@ static void _php_curl_reset_handlers(php_curl *ch)
ZVAL_UNDEF(&ch->handlers.std_err);
}

if (ZEND_FCC_INITIALIZED(ch->handlers.seek)) {
zend_fcc_dtor(&ch->handlers.seek);
}

if (ZEND_FCC_INITIALIZED(ch->handlers.progress)) {
zend_fcc_dtor(&ch->handlers.progress);
}
Expand Down
44 changes: 44 additions & 0 deletions ext/curl/tests/curl_copy_handle_seek.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
--TEST--
Test curl_copy_handle() with CURLOPT_SEEKFUNCTION
--EXTENSIONS--
curl
--FILE--
<?php
include 'server.inc';
$host = curl_cli_server_start();

$body = 'Hello cURL seek!';
$offset = 0;
$seekCalls = 0;

$ch = curl_init("{$host}/get.inc?test=redirect");
curl_setopt($ch, CURLOPT_UPLOAD, true);
curl_setopt($ch, CURLOPT_INFILESIZE, strlen($body));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $fd, int $length) use ($body, &$offset) {
$chunk = substr($body, $offset, $length);
$offset += strlen($chunk);
return $chunk;
});
curl_setopt($ch, CURLOPT_SEEKFUNCTION, function ($ch, int $position, int $origin) use (&$offset, &$seekCalls) {
if ($origin !== SEEK_SET) {
return CURL_SEEKFUNC_CANTSEEK;
}
$seekCalls++;
$offset = $position;
return CURL_SEEKFUNC_OK;
});

// The copied handle must inherit the seek callback; exercise it on the copy
// after freeing the original.
$ch2 = curl_copy_handle($ch);
unset($ch);

$response = curl_exec($ch2);
var_dump($seekCalls > 0);
var_dump(str_contains($response, $body));
?>
--EXPECT--
bool(true)
bool(true)
51 changes: 51 additions & 0 deletions ext/curl/tests/curl_seekfunction.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
--TEST--
CURLOPT_SEEKFUNCTION is called to rewind a streamed upload across a redirect
--EXTENSIONS--
curl
--FILE--
<?php
include 'server.inc';
$host = curl_cli_server_start();

$body = 'Hello cURL seek!';
$offset = 0;
$seekCalls = 0;
$argsChecked = false;

$ch = curl_init("{$host}/get.inc?test=redirect");
curl_setopt($ch, CURLOPT_UPLOAD, true);
curl_setopt($ch, CURLOPT_INFILESIZE, strlen($body));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $fd, int $length) use ($body, &$offset) {
$chunk = substr($body, $offset, $length);
$offset += strlen($chunk);
return $chunk;
});
curl_setopt($ch, CURLOPT_SEEKFUNCTION, function ($ch, int $position, int $origin) use (&$offset, &$seekCalls, &$argsChecked) {
if (!$argsChecked) {
$argsChecked = true;
var_dump($ch instanceof CurlHandle);
var_dump($position === 0);
var_dump($origin === SEEK_SET);
}
if ($origin !== SEEK_SET) {
return CURL_SEEKFUNC_CANTSEEK;
}
$seekCalls++;
$offset = $position;
return CURL_SEEKFUNC_OK;
});

$response = curl_exec($ch);
// The seek callback must have been invoked to rewind the body for the resend,
// and the resent body must have reached the redirect target intact.
var_dump($seekCalls > 0);
var_dump(str_contains($response, $body));
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
77 changes: 77 additions & 0 deletions ext/curl/tests/curl_seekfunction_error.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
--TEST--
CURLOPT_SEEKFUNCTION callback error handling and option validation
--EXTENSIONS--
curl
--FILE--
<?php
include 'server.inc';
$host = curl_cli_server_start();

// Drive a 307-redirect upload so libcurl invokes the seek callback to rewind
// the body; $seek is the callback under test.
function run_upload(string $host, callable $seek): void
{
$offset = 0;
$body = 'Hello cURL seek!';
$ch = curl_init("{$host}/get.inc?test=redirect");
curl_setopt($ch, CURLOPT_UPLOAD, true);
curl_setopt($ch, CURLOPT_INFILESIZE, strlen($body));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $fd, int $length) use ($body, &$offset) {
$chunk = substr($body, $offset, $length);
$offset += strlen($chunk);
return $chunk;
});
curl_setopt($ch, CURLOPT_SEEKFUNCTION, $seek);
curl_exec($ch);
}

echo "Returning a non-int:\n";
try {
run_upload($host, fn($ch, $offset, $origin) => 'not an int');
} catch (\TypeError $e) {
echo $e->getMessage(), "\n";
}

echo "\nReturning an out-of-range int:\n";
try {
run_upload($host, fn($ch, $offset, $origin) => 42);
} catch (\ValueError $e) {
echo $e->getMessage(), "\n";
}

echo "\nThrowing from the callback:\n";
try {
run_upload($host, function ($ch, $offset, $origin) {
throw new \RuntimeException('boom from seek');
});
} catch (\RuntimeException $e) {
echo $e->getMessage(), "\n";
}

echo "\nSetting the callback to null:\n";
var_dump(curl_setopt(curl_init(), CURLOPT_SEEKFUNCTION, null));

echo "\nSetting a non-callable scalar:\n";
try {
curl_setopt(curl_init(), CURLOPT_SEEKFUNCTION, 42);
} catch (\TypeError $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
Returning a non-int:
The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK

Returning an out-of-range int:
The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK

Throwing from the callback:
boom from seek

Setting the callback to null:
bool(true)

Setting a non-callable scalar:
curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_SEEKFUNCTION, no array or string given
Loading
Loading