From 78e437aab31c4843352a9d99dbd21961a852c205 Mon Sep 17 00:00:00 2001 From: Derick Rethans Date: Fri, 1 Apr 2022 16:43:35 +0100 Subject: [PATCH] Implemented FR #51634: Can't post multiple fields with the same name This PR allows for multiple values with the same field name to be sent to an HTTP server. This server can't be PHP, because PHP's POST processing does not allow for two the same field names. But sending multiple fields with the same name is required for some operations by RFC 7578. This PR allows you to attach an array of values to a field name, and each of these will then be sent as its own distinct multipart/form-data field. --- ext/curl/interface.c | 85 ++++++++++++++++------- ext/curl/tests/curl_postfields_array.phpt | 63 +++++++++++++++++ 2 files changed, 121 insertions(+), 27 deletions(-) create mode 100644 ext/curl/tests/curl_postfields_array.phpt diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 3292458d63554..0062586c64ec9 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -1991,6 +1991,54 @@ static void free_cb(void *arg) /* {{{ */ /* }}} */ #endif +static inline CURLcode add_simple_field(curl_mime *mime, zend_string *string_key, zval *current) +{ + CURLcode error = CURLE_OK; +#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */ + curl_mimepart *part; + CURLcode form_error; +#else + struct HttpPost *first = NULL; + struct HttpPost *last = NULL; + CURLFORMcode form_error; +#endif + zend_string *postval, *tmp_postval; + + postval = zval_get_tmp_string(current, &tmp_postval); + +#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */ + part = curl_mime_addpart(mime); + if (part == NULL) { + zend_tmp_string_release(tmp_postval); + zend_string_release_ex(string_key, 0); + 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) { + error = form_error; + } +#else + /* The arguments after _NAMELENGTH and _CONTENTSLENGTH + * must be explicitly cast to long in curl_formadd + * use since curl needs a long not an int. */ + form_error = curl_formadd(&first, &last, + CURLFORM_COPYNAME, ZSTR_VAL(string_key), + CURLFORM_NAMELENGTH, ZSTR_LEN(string_key), + CURLFORM_COPYCONTENTS, ZSTR_VAL(postval), + CURLFORM_CONTENTSLENGTH, ZSTR_LEN(postval), + CURLFORM_END + ); + + if (form_error != CURL_FORMADD_OK) { + /* Not nice to convert between enums but we only have place for one error type */ + error = (CURLcode)form_error; + } +#endif + zend_tmp_string_release(tmp_postval); + + return error; +} + static inline zend_result build_mime_structure_from_hash(php_curl *ch, zval *zpostfields) /* {{{ */ { HashTable *postfields = Z_ARRVAL_P(zpostfields); @@ -2018,7 +2066,7 @@ static inline zend_result build_mime_structure_from_hash(php_curl *ch, zval *zpo #endif ZEND_HASH_FOREACH_KEY_VAL(postfields, num_key, string_key, current) { - zend_string *postval, *tmp_postval; + zend_string *postval; /* Pretend we have a string_key here */ if (!string_key) { string_key = zend_long_to_str(num_key); @@ -2181,36 +2229,19 @@ static inline zend_result build_mime_structure_from_hash(php_curl *ch, zval *zpo continue; } - postval = zval_get_tmp_string(current, &tmp_postval); + if (Z_TYPE_P(current) == IS_ARRAY) { + zval *current_element; + + ZEND_HASH_FOREACH_VAL(HASH_OF(current), current_element) { + add_simple_field(mime, string_key, current_element); + } ZEND_HASH_FOREACH_END(); -#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */ - part = curl_mime_addpart(mime); - if (part == NULL) { - zend_tmp_string_release(tmp_postval); zend_string_release_ex(string_key, 0); - return FAILURE; - } - 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) { - error = form_error; + continue; } -#else - /* The arguments after _NAMELENGTH and _CONTENTSLENGTH - * must be explicitly cast to long in curl_formadd - * use since curl needs a long not an int. */ - form_error = curl_formadd(&first, &last, - CURLFORM_COPYNAME, ZSTR_VAL(string_key), - CURLFORM_NAMELENGTH, ZSTR_LEN(string_key), - CURLFORM_COPYCONTENTS, ZSTR_VAL(postval), - CURLFORM_CONTENTSLENGTH, ZSTR_LEN(postval), - CURLFORM_END); - if (form_error != CURL_FORMADD_OK) { - /* Not nice to convert between enums but we only have place for one error type */ - error = (CURLcode)form_error; - } -#endif - zend_tmp_string_release(tmp_postval); + add_simple_field(mime, string_key, current); + zend_string_release_ex(string_key, 0); } ZEND_HASH_FOREACH_END(); diff --git a/ext/curl/tests/curl_postfields_array.phpt b/ext/curl/tests/curl_postfields_array.phpt new file mode 100644 index 0000000000000..4e61c85f1b386 --- /dev/null +++ b/ext/curl/tests/curl_postfields_array.phpt @@ -0,0 +1,63 @@ +--TEST-- +CURLOPT_POSTFIELDS with multi-value fields +--EXTENSIONS-- +sockets +--FILE-- +\n"; + return; +} + +$url = "http://127.0.0.1:29999/get.inc?test=raw"; + +$fields = [ + 'single' => 'SingleValue', + 'multi' => [ + 'Multi1', + 'Multi2', + ] +]; + +$options = array( + 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, +); + +$ch = curl_init(); +curl_setopt_array($ch, $options); + +$curl_content = curl_exec($ch); +curl_close($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" + +Multi1 +--------------------------%s +Content-Disposition: form-data; name="multi" + +Multi2 +--------------------------%s--