Commit 2544d0b7d91 for php
commit 2544d0b7d911850f738fb77f3afc81bc5f531b54
Author: Sjoerd Langkemper <sjoerd-github@linuxonly.nl>
Date: Wed Oct 14 19:28:54 2026 +0200
ext/curl: post multiple similarly-named files (#22740)
Make it possible to post multiple files with the same name using cURL.
It was already possible to post files, and it was possible to post
multiple strings with the same name. This moves things around to combine
these two features.
Closes GH-15859
diff --git a/NEWS b/NEWS
index 9258c398f1e..d64960d02cb 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,9 @@ PHP NEWS
- Curl:
. curl_multi_exec() now throws a TypeError when still_running is not an int.
(arshidkv12)
+ . Uploading multiple files with the same field name is now possible by
+ passing an array of CURLFile or CURLStringFile objects to
+ CURLOPT_POSTFIELDS. (Sjoerd Langkemper)
- FPM:
. Use recommended TYPE, HELP order for OpenMetrics metadata.
diff --git a/UPGRADING b/UPGRADING
index ac335b4e5aa..61f6b492954 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -21,6 +21,8 @@ PHP 8.7 UPGRADE NOTES
- Curl:
. curl_multi_exec() now throws a TypeError when still_running is not an int.
+ . Nested arrays in CURLOPT_POSTFIELDS now throw a TypeError, instead of being
+ silently converted to the string "Array".
- PDO:
. PDOStatement::bindColumn() now throws a ValueError when the column name is
@@ -43,6 +45,10 @@ PHP 8.7 UPGRADE NOTES
2. New Features
========================================
+- Curl:
+ . Multiple files can now be uploaded with the same field name by passing an
+ array of CURLFile or CURLStringFile objects to CURLOPT_POSTFIELDS.
+
- Intl:
. Locale::acceptFromHttp() and locale_accept_from_http() now accept an
optional $availableLocales array argument. When given, the
diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index d1f0cee3d67..61b42322089 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -1413,173 +1413,198 @@ static inline CURLcode add_simple_field(curl_mime *mime, zend_string *string_key
return error;
}
-static inline zend_result build_mime_structure_from_hash(php_curl *ch, zval *zpostfields) /* {{{ */
+static inline CURLcode add_form_field(php_curl *ch, curl_mime *mime, zend_string *string_key, zval *current, zval *zpostfields)
{
- HashTable *postfields = Z_ARRVAL_P(zpostfields);
+ zend_string *postval;
CURLcode error = CURLE_OK;
- zval *current;
- zend_string *string_key;
- zend_ulong num_key;
- curl_mime *mime = NULL;
- curl_mimepart *part;
CURLcode form_error;
+ curl_mimepart *part;
- if (zend_hash_num_elements(postfields) > 0) {
- mime = curl_mime_init(ch->cp);
- if (mime == NULL) {
- return FAILURE;
- }
+ ZVAL_DEREF(current);
+
+ if (Z_TYPE_P(current) == IS_ARRAY) {
+ zend_type_error("%s(): Nested arrays are not supported for key %s", get_active_function_name(), ZSTR_VAL(string_key));
+ return CURLE_BAD_FUNCTION_ARGUMENT;
}
- ZEND_HASH_FOREACH_KEY_VAL(postfields, num_key, string_key, current) {
- zend_string *postval;
- /* Pretend we have a string_key here */
- if (!string_key) {
- string_key = zend_long_to_str(num_key);
+ if (Z_TYPE_P(current) == IS_OBJECT &&
+ instanceof_function(Z_OBJCE_P(current), curl_CURLFile_class)) {
+ /* new-style file upload */
+ zval *prop, rv;
+ char *type = NULL, *filename = NULL;
+ struct mime_data_cb_arg *cb_arg;
+ php_stream_statbuf ssb;
+ size_t filesize = -1;
+ curl_seek_callback seekfunc = seek_cb;
+
+ prop = zend_read_property_ex(curl_CURLFile_class, Z_OBJ_P(current), ZSTR_KNOWN(ZEND_STR_NAME), /* silent */ false, &rv);
+ ZVAL_DEREF(prop);
+ if (Z_TYPE_P(prop) != IS_STRING) {
+ php_error_docref(NULL, E_WARNING, "Invalid filename for key %s", ZSTR_VAL(string_key));
} else {
- zend_string_addref(string_key);
- }
+ postval = Z_STR_P(prop);
- ZVAL_DEREF(current);
- if (Z_TYPE_P(current) == IS_OBJECT &&
- instanceof_function(Z_OBJCE_P(current), curl_CURLFile_class)) {
- /* new-style file upload */
- zval *prop, rv;
- char *type = NULL, *filename = NULL;
- struct mime_data_cb_arg *cb_arg;
- php_stream_statbuf ssb;
- size_t filesize = -1;
- curl_seek_callback seekfunc = seek_cb;
-
- prop = zend_read_property_ex(curl_CURLFile_class, Z_OBJ_P(current), ZSTR_KNOWN(ZEND_STR_NAME), /* silent */ false, &rv);
+ if (php_check_open_basedir(ZSTR_VAL(postval))) {
+ return CURLE_ABORTED_BY_CALLBACK;
+ }
+
+ prop = zend_read_property(curl_CURLFile_class, Z_OBJ_P(current), "mime", sizeof("mime")-1, 0, &rv);
ZVAL_DEREF(prop);
- if (Z_TYPE_P(prop) != IS_STRING) {
- php_error_docref(NULL, E_WARNING, "Invalid filename for key %s", ZSTR_VAL(string_key));
- } else {
- postval = Z_STR_P(prop);
+ if (Z_TYPE_P(prop) == IS_STRING && Z_STRLEN_P(prop) > 0) {
+ type = Z_STRVAL_P(prop);
+ }
+ prop = zend_read_property(curl_CURLFile_class, Z_OBJ_P(current), "postname", sizeof("postname")-1, 0, &rv);
+ ZVAL_DEREF(prop);
+ if (Z_TYPE_P(prop) == IS_STRING && Z_STRLEN_P(prop) > 0) {
+ filename = Z_STRVAL_P(prop);
+ }
- if (php_check_open_basedir(ZSTR_VAL(postval))) {
- goto out_string;
- }
+ zval_ptr_dtor(&ch->postfields);
+ ZVAL_COPY(&ch->postfields, zpostfields);
- prop = zend_read_property(curl_CURLFile_class, Z_OBJ_P(current), "mime", sizeof("mime")-1, 0, &rv);
- ZVAL_DEREF(prop);
- if (Z_TYPE_P(prop) == IS_STRING && Z_STRLEN_P(prop) > 0) {
- type = Z_STRVAL_P(prop);
+ php_stream *stream;
+ if ((stream = php_stream_open_wrapper(ZSTR_VAL(postval), "rb", STREAM_MUST_SEEK, NULL))) {
+ if (!stream->readfilters.head && !php_stream_stat(stream, &ssb)) {
+ filesize = ssb.sb.st_size;
}
- prop = zend_read_property(curl_CURLFile_class, Z_OBJ_P(current), "postname", sizeof("postname")-1, 0, &rv);
- ZVAL_DEREF(prop);
- if (Z_TYPE_P(prop) == IS_STRING && Z_STRLEN_P(prop) > 0) {
- filename = Z_STRVAL_P(prop);
+ } else {
+ seekfunc = NULL;
+ }
+
+ part = curl_mime_addpart(mime);
+ if (part == NULL) {
+ if (stream) {
+ php_stream_close(stream);
}
+ return CURLE_OUT_OF_MEMORY;
+ }
- zval_ptr_dtor(&ch->postfields);
- ZVAL_COPY(&ch->postfields, zpostfields);
+ cb_arg = emalloc(sizeof *cb_arg);
+ cb_arg->filename = zend_string_copy(postval);
+ cb_arg->stream = stream;
- php_stream *stream;
- if ((stream = php_stream_open_wrapper(ZSTR_VAL(postval), "rb", STREAM_MUST_SEEK, NULL))) {
- if (!stream->readfilters.head && !php_stream_stat(stream, &ssb)) {
- filesize = ssb.sb.st_size;
- }
- } else {
- seekfunc = NULL;
- }
+ if ((form_error = curl_mime_name(part, ZSTR_VAL(string_key))) != CURLE_OK
+ || (form_error = curl_mime_data_cb(part, filesize, read_cb, seekfunc, free_cb, cb_arg)) != CURLE_OK
+ || (form_error = curl_mime_filename(part, filename ? filename : ZSTR_VAL(postval))) != CURLE_OK
+ || (form_error = curl_mime_type(part, type ? type : "application/octet-stream")) != CURLE_OK) {
+ error = form_error;
+ }
+ zend_llist_add_element(&ch->to_free->stream, &cb_arg);
+ }
- part = curl_mime_addpart(mime);
- if (part == NULL) {
- if (stream) {
- php_stream_close(stream);
- }
- goto out_string;
- }
+ return error;
+ }
- cb_arg = emalloc(sizeof *cb_arg);
- cb_arg->filename = zend_string_copy(postval);
- cb_arg->stream = stream;
+ if (Z_TYPE_P(current) == IS_OBJECT && instanceof_function(Z_OBJCE_P(current), curl_CURLStringFile_class)) {
+ /* new-style file upload from string */
+ zval *prop, rv;
+ char *type = NULL, *filename = NULL;
- if ((form_error = curl_mime_name(part, ZSTR_VAL(string_key))) != CURLE_OK
- || (form_error = curl_mime_data_cb(part, filesize, read_cb, seekfunc, free_cb, cb_arg)) != CURLE_OK
- || (form_error = curl_mime_filename(part, filename ? filename : ZSTR_VAL(postval))) != CURLE_OK
- || (form_error = curl_mime_type(part, type ? type : "application/octet-stream")) != CURLE_OK) {
- error = form_error;
- }
- zend_llist_add_element(&ch->to_free->stream, &cb_arg);
- }
+ prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "postname", sizeof("postname")-1, 0, &rv);
+ if (EG(exception)) {
+ return CURLE_BAD_FUNCTION_ARGUMENT;
+ }
+ ZVAL_DEREF(prop);
+ ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING);
- zend_string_release_ex(string_key, 0);
- continue;
+ filename = Z_STRVAL_P(prop);
+
+ prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "mime", sizeof("mime")-1, 0, &rv);
+ if (EG(exception)) {
+ return CURLE_BAD_FUNCTION_ARGUMENT;
}
+ ZVAL_DEREF(prop);
+ ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING);
- if (Z_TYPE_P(current) == IS_OBJECT && instanceof_function(Z_OBJCE_P(current), curl_CURLStringFile_class)) {
- /* new-style file upload from string */
- zval *prop, rv;
- char *type = NULL, *filename = NULL;
+ type = Z_STRVAL_P(prop);
- prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "postname", sizeof("postname")-1, 0, &rv);
- if (EG(exception)) {
- goto out_string;
- }
- ZVAL_DEREF(prop);
- ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING);
+ prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "data", sizeof("data")-1, 0, &rv);
+ if (EG(exception)) {
+ return CURLE_BAD_FUNCTION_ARGUMENT;
+ }
+ ZVAL_DEREF(prop);
+ ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING);
- filename = Z_STRVAL_P(prop);
+ postval = Z_STR_P(prop);
- prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "mime", sizeof("mime")-1, 0, &rv);
- if (EG(exception)) {
- goto out_string;
- }
- ZVAL_DEREF(prop);
- ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING);
+ zval_ptr_dtor(&ch->postfields);
+ ZVAL_COPY(&ch->postfields, zpostfields);
- type = Z_STRVAL_P(prop);
+ part = curl_mime_addpart(mime);
+ if (part == NULL) {
+ return CURLE_OUT_OF_MEMORY;
+ }
+ if ((form_error = curl_mime_name(part, ZSTR_VAL(string_key))) != CURLE_OK
+ || (form_error = curl_mime_data(part, ZSTR_VAL(postval), ZSTR_LEN(postval))) != CURLE_OK
+ || (form_error = curl_mime_filename(part, filename)) != CURLE_OK
+ || (form_error = curl_mime_type(part, type)) != CURLE_OK) {
+ error = form_error;
+ }
- prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "data", sizeof("data")-1, 0, &rv);
- if (EG(exception)) {
- goto out_string;
- }
- ZVAL_DEREF(prop);
- ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING);
+ return error;
+ }
- postval = Z_STR_P(prop);
+ return add_simple_field(mime, string_key, current);
+}
- zval_ptr_dtor(&ch->postfields);
- ZVAL_COPY(&ch->postfields, zpostfields);
- part = curl_mime_addpart(mime);
- if (part == NULL) {
- goto out_string;
- }
- if ((form_error = curl_mime_name(part, ZSTR_VAL(string_key))) != CURLE_OK
- || (form_error = curl_mime_data(part, ZSTR_VAL(postval), ZSTR_LEN(postval))) != CURLE_OK
- || (form_error = curl_mime_filename(part, filename)) != CURLE_OK
- || (form_error = curl_mime_type(part, type)) != CURLE_OK) {
- error = form_error;
- }
+static inline zend_result build_mime_structure_from_hash(php_curl *ch, zval *zpostfields) /* {{{ */
+{
+ HashTable *postfields = Z_ARRVAL_P(zpostfields);
+ CURLcode error = CURLE_OK;
+ zval *current;
+ zend_string *string_key;
+ zend_ulong num_key;
+ curl_mime *mime = NULL;
- zend_string_release_ex(string_key, 0);
- continue;
+ if (zend_hash_num_elements(postfields) > 0) {
+ mime = curl_mime_init(ch->cp);
+ if (mime == NULL) {
+ SAVE_CURL_ERROR(ch, CURLE_OUT_OF_MEMORY);
+ return FAILURE;
}
+ }
+
+ ZEND_HASH_FOREACH_KEY_VAL(postfields, num_key, string_key, current) {
+ /* Pretend we have a string_key here */
+ if (!string_key) {
+ string_key = zend_long_to_str(num_key);
+ } else {
+ zend_string_addref(string_key);
+ }
+
+ ZVAL_DEREF(current);
if (Z_TYPE_P(current) == IS_ARRAY) {
zval *current_element;
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(current), current_element) {
- add_simple_field(mime, string_key, current_element);
+ ZVAL_DEREF(current_element);
+
+ error = add_form_field(
+ ch, mime, string_key, current_element, zpostfields
+ );
+
+ if (error != CURLE_OK) {
+ zend_string_release_ex(string_key, 0);
+ goto out_mime;
+ }
} ZEND_HASH_FOREACH_END();
zend_string_release_ex(string_key, 0);
continue;
}
- add_simple_field(mime, string_key, current);
+ error = add_form_field(
+ ch, mime, string_key, current, zpostfields
+ );
zend_string_release_ex(string_key, 0);
- } ZEND_HASH_FOREACH_END();
- SAVE_CURL_ERROR(ch, error);
- if (error != CURLE_OK) {
- goto out_mime;
- }
+ if (error != CURLE_OK) {
+ goto out_mime;
+ }
+ } ZEND_HASH_FOREACH_END();
if ((*ch->clone) == 1) {
zend_llist_clean(&ch->to_free->post);
@@ -1590,9 +1615,8 @@ static inline zend_result build_mime_structure_from_hash(php_curl *ch, zval *zpo
SAVE_CURL_ERROR(ch, error);
return error == CURLE_OK ? SUCCESS : FAILURE;
-out_string:
- zend_string_release_ex(string_key, false);
out_mime:
+ SAVE_CURL_ERROR(ch, error);
curl_mime_free(mime);
return FAILURE;
}
diff --git a/ext/curl/tests/curl_postfields_error.phpt b/ext/curl/tests/curl_postfields_error.phpt
new file mode 100644
index 00000000000..2e667915b53
--- /dev/null
+++ b/ext/curl/tests/curl_postfields_error.phpt
@@ -0,0 +1,37 @@
+--TEST--
+CURLOPT_POSTFIELDS with nested arrays
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+
+$fields = [
+ 'single' => 'SingleValue',
+ 'multi' => [
+ ['array', 'invalid' => 'value'],
+ 'Multi2',
+ ]
+];
+
+$options = [
+ CURLOPT_POST => 1,
+ CURLOPT_HEADER => 0,
+ CURLOPT_FRESH_CONNECT => 1,
+ CURLOPT_RETURNTRANSFER => 1,
+ CURLOPT_FORBID_REUSE => 1,
+ CURLOPT_TIMEOUT => 1,
+ CURLOPT_POSTFIELDS => $fields,
+ CURLOPT_HTTPHEADER => [ 'Expect:' ],
+];
+
+try {
+ $ch = curl_init();
+ curl_setopt_array($ch, $options);
+} catch (TypeError $e) {
+ echo "TypeError: {$e->getMessage()}\n";
+}
+var_dump(curl_errno($ch) === CURLE_BAD_FUNCTION_ARGUMENT);
+?>
+--EXPECT--
+TypeError: curl_setopt_array(): Nested arrays are not supported for key multi
+bool(true)
diff --git a/ext/curl/tests/curl_postfiles_array.phpt b/ext/curl/tests/curl_postfiles_array.phpt
new file mode 100644
index 00000000000..887ff291c4a
--- /dev/null
+++ b/ext/curl/tests/curl_postfiles_array.phpt
@@ -0,0 +1,103 @@
+--TEST--
+CURLOPT_POSTFIELDS with multi-value files
+--EXTENSIONS--
+curl
+sockets
+--FILE--
+<?php
+$socket = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr);
+
+if (!$socket) {
+ echo "$errstr ($errno)<br />\n";
+ return;
+}
+
+$addr = stream_socket_get_name($socket, false);
+$url = "http://$addr/get.inc?test=raw";
+
+$fields = [
+ 'single' => 'SingleValue',
+ 'multi_string' => [
+ new CURLStringFile('Multi1', 'multi1.txt', 'text/plain'),
+ new CURLStringFile('Multi2', 'multi2.dat'),
+ ],
+ 'multi_file' => [
+ new CURLFile(__DIR__ . '/curl_testdata1.txt'),
+ new CURLFile(__DIR__ . '/curl_testdata2.txt'),
+ ],
+ 'mixed' => [
+ 'string value',
+ new CURLStringFile('curl string file', 'stringfile.txt', 'text/plain'),
+ new CURLFile(__DIR__ . '/curl_testdata1.txt', 'text/plain'),
+ ]
+];
+
+$options = [
+ CURLOPT_POST => 1,
+ CURLOPT_HEADER => 0,
+ CURLOPT_URL => $url,
+ CURLOPT_FRESH_CONNECT => 1,
+ CURLOPT_RETURNTRANSFER => 1,
+ CURLOPT_FORBID_REUSE => 1,
+ CURLOPT_TIMEOUT => 1,
+ CURLOPT_POSTFIELDS => $fields,
+ CURLOPT_HTTPHEADER => [ 'Expect:' ],
+];
+
+$ch = curl_init();
+curl_setopt_array($ch, $options);
+
+$curl_content = curl_exec($ch);
+
+$conn = stream_socket_accept($socket);
+echo stream_get_contents($conn);
+?>
+--EXPECTF--
+POST /get.inc?test=raw HTTP/1.1
+Host: %s
+Accept: */*
+Content-Length: %d
+Content-Type: multipart/form-data; boundary=------------------------%s
+
+--------------------------%s
+Content-Disposition: form-data; name="single"
+
+SingleValue
+--------------------------%s
+Content-Disposition: form-data; name="multi_string"; filename="multi1.txt"
+Content-Type: text/plain
+
+Multi1
+--------------------------%s
+Content-Disposition: form-data; name="multi_string"; filename="multi2.dat"
+Content-Type: application/octet-stream
+
+Multi2
+--------------------------%s
+Content-Disposition: form-data; name="multi_file"; filename="%s"
+Content-Type: application/octet-stream
+
+CURL1
+
+--------------------------%s
+Content-Disposition: form-data; name="multi_file"; filename="%s"
+Content-Type: application/octet-stream
+
+CURL2
+
+--------------------------%s
+Content-Disposition: form-data; name="mixed"
+
+string value
+--------------------------%s
+Content-Disposition: form-data; name="mixed"; filename="stringfile.txt"
+Content-Type: text/plain
+
+curl string file
+--------------------------%s
+Content-Disposition: form-data; name="mixed"; filename="%s"
+Content-Type: text/plain
+
+CURL1
+
+--------------------------%s--