Commit 93fa5f3c882 for php.net
commit 93fa5f3c882e5b51342fa0674d25d5d29d7a0546
Author: Graham Campbell <hello@gjcampbell.co.uk>
Date: Fri Jun 5 15:51:50 2026 +0100
ext/curl: add CURLOPT_SEEKFUNCTION
diff --git a/NEWS b/NEWS
index 190c6c16a93..4be8d1e554c 100644
--- a/NEWS
+++ b/NEWS
@@ -14,6 +14,12 @@ PHP NEWS
. Fixed bug GH-22602 (gregoriantojd() and juliantojd() integer overflow with
INT_MAX year). (arshidkv12)
+- 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.17. (Derick)
. Fixed bug GH-19803 (Parsing a string with a single white space does create
diff --git a/UPGRADING b/UPGRADING
index 031b6751d85..a809687d196 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -238,6 +238,12 @@ PHP 8.6 UPGRADE NOTES
This value can also be obtained by passing CURLINFO_SIZE_DELIVERED as the
$option parameter.
Requires libcurl 8.20.0 or later.
+ . 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.
@@ -467,6 +473,10 @@ PHP 8.6 UPGRADE NOTES
- Curl:
. CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0).
+ . CURLOPT_SEEKFUNCTION.
+ . CURL_SEEKFUNC_OK.
+ . CURL_SEEKFUNC_FAIL.
+ . CURL_SEEKFUNC_CANTSEEK.
- Sockets:
. TCP_USER_TIMEOUT (Linux only).
diff --git a/ext/curl/curl.stub.php b/ext/curl/curl.stub.php
index d3bd58510e4..70e87cc9b14 100644
--- a/ext/curl/curl.stub.php
+++ b/ext/curl/curl.stub.php
@@ -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
@@ -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
diff --git a/ext/curl/curl_arginfo.h b/ext/curl/curl_arginfo.h
index 6a05b73572c..f2929f60c4e 100644
Binary files a/ext/curl/curl_arginfo.h and b/ext/curl/curl_arginfo.h differ
diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h
index 77b0628ee42..25cc37b6e45 100644
--- a/ext/curl/curl_private.h
+++ b/ext/curl/curl_private.h
@@ -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;
diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 09c7009068b..b7ec015abf6 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -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);
}
@@ -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)
{
@@ -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;
@@ -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);
@@ -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);
@@ -2794,6 +2847,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);
}
@@ -2878,6 +2934,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);
}
diff --git a/ext/curl/tests/curl_copy_handle_seek.phpt b/ext/curl/tests/curl_copy_handle_seek.phpt
new file mode 100644
index 00000000000..c2a2f37bbe1
--- /dev/null
+++ b/ext/curl/tests/curl_copy_handle_seek.phpt
@@ -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)
diff --git a/ext/curl/tests/curl_seekfunction.phpt b/ext/curl/tests/curl_seekfunction.phpt
new file mode 100644
index 00000000000..d2daa1e7d8d
--- /dev/null
+++ b/ext/curl/tests/curl_seekfunction.phpt
@@ -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)
diff --git a/ext/curl/tests/curl_seekfunction_error.phpt b/ext/curl/tests/curl_seekfunction_error.phpt
new file mode 100644
index 00000000000..134e8115dc5
--- /dev/null
+++ b/ext/curl/tests/curl_seekfunction_error.phpt
@@ -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
diff --git a/ext/curl/tests/curl_setopt_callables.phpt b/ext/curl/tests/curl_setopt_callables.phpt
index aaa83102afa..9de8d670571 100644
--- a/ext/curl/tests/curl_setopt_callables.phpt
+++ b/ext/curl/tests/curl_setopt_callables.phpt
@@ -27,6 +27,7 @@ function testOption(CurlHandle $handle, int $option) {
testOption($ch, CURLOPT_WRITEFUNCTION);
testOption($ch, CURLOPT_HEADERFUNCTION);
testOption($ch, CURLOPT_READFUNCTION);
+testOption($ch, CURLOPT_SEEKFUNCTION);
?>
--EXPECT--
@@ -42,3 +43,5 @@ function testOption(CurlHandle $handle, int $option) {
TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_HEADERFUNCTION, function "undefined" not found or invalid function name
TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_READFUNCTION, function "undefined" not found or invalid function name
TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_READFUNCTION, function "undefined" not found or invalid function name
+TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_SEEKFUNCTION, function "undefined" not found or invalid function name
+TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_SEEKFUNCTION, function "undefined" not found or invalid function name
diff --git a/ext/curl/tests/responder/get.inc b/ext/curl/tests/responder/get.inc
index c139c8c7d43..2ef1e4a89dd 100644
--- a/ext/curl/tests/responder/get.inc
+++ b/ext/curl/tests/responder/get.inc
@@ -46,6 +46,11 @@
case 'method':
echo $_SERVER['REQUEST_METHOD'];
break;
+ case 'redirect':
+ // A 307 preserves the method and body, so libcurl must rewind the upload
+ // (via CURLOPT_SEEKFUNCTION) before resending it to the new location.
+ header('Location: /get.inc?test=input', true, 307);
+ break;
default:
echo "Hello World!\n";
echo "Hello World!";