Merged
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 numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions UPGRADING
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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).
Expand Down
20 changes: 20 additions & 0 deletions ext/curl/curl.stub.php
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -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);
}
Expand DownExpand Up@@ -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);
}
Expand Down
44 changes: 44 additions & 0 deletions ext/curl/tests/curl_copy_handle_seek.phpt
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions UPGRADING
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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).
Expand Down
20 changes: 20 additions & 0 deletions ext/curl/curl.stub.php
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -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);
}
Expand DownExpand Up@@ -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);
}
Expand Down
44 changes: 44 additions & 0 deletions ext/curl/tests/curl_copy_handle_seek.phpt
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions UPGRADING
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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).
Expand Down
20 changes: 20 additions & 0 deletions ext/curl/curl.stub.php
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -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);
}
Expand DownExpand Up@@ -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);
}
Expand Down
44 changes: 44 additions & 0 deletions ext/curl/tests/curl_copy_handle_seek.phpt
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions UPGRADING
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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).
Expand Down
20 changes: 20 additions & 0 deletions ext/curl/curl.stub.php
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -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);
}
Expand DownExpand Up@@ -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);
}
Expand Down
44 changes: 44 additions & 0 deletions ext/curl/tests/curl_copy_handle_seek.phpt
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions UPGRADING
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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).
Expand Down
20 changes: 20 additions & 0 deletions ext/curl/curl.stub.php
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -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);
}
Expand DownExpand Up@@ -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);
}
Expand Down
44 changes: 44 additions & 0 deletions ext/curl/tests/curl_copy_handle_seek.phpt
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions UPGRADING
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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).
Expand Down
20 changes: 20 additions & 0 deletions ext/curl/curl.stub.php
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -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);
}
Expand DownExpand Up@@ -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);
}
Expand Down
44 changes: 44 additions & 0 deletions ext/curl/tests/curl_copy_handle_seek.phpt
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions UPGRADING
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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).
Expand Down
20 changes: 20 additions & 0 deletions ext/curl/curl.stub.php
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -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);
}
Expand DownExpand Up@@ -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);
}
Expand Down
44 changes: 44 additions & 0 deletions ext/curl/tests/curl_copy_handle_seek.phpt
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions UPGRADING
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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).
Expand Down
20 changes: 20 additions & 0 deletions ext/curl/curl.stub.php
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -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);
}
Expand DownExpand Up@@ -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);
}
Expand Down
44 changes: 44 additions & 0 deletions ext/curl/tests/curl_copy_handle_seek.phpt
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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