From aece9975d7245639b334d210ff82359974e6cbb1 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Wed, 15 Jul 2026 11:50:40 +0000 Subject: [PATCH] Post multiple similarly-named files with curl 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 https://github.com/php/php-src/issues/15859 --- ext/curl/interface.c | 266 ++++++++++++---------- ext/curl/tests/curl_postfields_error.phpt | 39 ++++ ext/curl/tests/curl_postfiles_array.phpt | 102 +++++++++ 3 files changed, 287 insertions(+), 120 deletions(-) create mode 100644 ext/curl/tests/curl_postfields_error.phpt create mode 100644 ext/curl/tests/curl_postfiles_array.phpt diff --git a/ext/curl/interface.c b/ext/curl/interface.c index c0286069776e..92d9eecfe280 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -1361,167 +1361,195 @@ 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) /* {{{ */ -{ - HashTable *postfields = Z_ARRVAL_P(zpostfields); +static inline CURLcode add_form_field(php_curl *ch, curl_mime *mime, zend_string *string_key, zval *current, zval *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) { + 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); + + if (error != CURLE_OK) { + goto out_mime; + } } ZEND_HASH_FOREACH_END(); SAVE_CURL_ERROR(ch, error); @@ -1538,8 +1566,6 @@ 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: 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 000000000000..c41bc111b5e7 --- /dev/null +++ b/ext/curl/tests/curl_postfields_error.phpt @@ -0,0 +1,39 @@ +--TEST-- +CURLOPT_POSTFIELDS with nested arrays +--EXTENSIONS-- +curl +sockets +--FILE-- + 'SingleValue', + 'multi' => [ + ['array', 'invalid' => 'value'], + 'Multi2', + ] +]; + +$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:' ], +]; + +try { + $ch = curl_init(); + curl_setopt_array($ch, $options); +} catch (TypeError $e) { + echo "TypeError: {$e->getMessage()}\n"; +} +?> +--EXPECTF-- +TypeError: curl_setopt_array(): Nested arrays are not supported for key multi diff --git a/ext/curl/tests/curl_postfiles_array.phpt b/ext/curl/tests/curl_postfiles_array.phpt new file mode 100644 index 000000000000..42eda68afa2c --- /dev/null +++ b/ext/curl/tests/curl_postfiles_array.phpt @@ -0,0 +1,102 @@ +--TEST-- +CURLOPT_POSTFIELDS with multi-value files +--EXTENSIONS-- +curl +sockets +--FILE-- +\n"; + return; +} + +$url = "http://127.0.0.1:29999/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--