From e727919b97c4baeca93cb7700d4adf2e35a3530f Mon Sep 17 00:00:00 2001 From: Alexander Moskalev Date: Thu, 26 Nov 2020 10:30:02 +0300 Subject: [PATCH] cURL: make possible to send file from buffer string Add CURLStringFile class which works similarly to CURLFile, but uploads a file from a string rather than a file. This avoids the need to create a temporary file, or use of a data:// stream. Basic usage: $file = new CURLStringFile($data, 'filename.txt', 'text/plain'); curl_setopt($curl, CURLOPT_POSTFIELDS, ['file' => $file]); Closes GH-6456. --- UPGRADING | 5 ++ ext/curl/curl_file.c | 26 ++++++ ext/curl/curl_file.stub.php | 9 +++ ext/curl/curl_file_arginfo.h | 43 +++++++++- ext/curl/curl_private.h | 3 + ext/curl/interface.c | 90 +++++++++++++++++++++ ext/curl/php_curl.h | 1 + ext/curl/tests/curl_string_file_upload.phpt | 86 ++++++++++++++++++++ ext/curl/tests/responder/get.inc | 9 +++ 9 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 ext/curl/tests/curl_string_file_upload.phpt diff --git a/UPGRADING b/UPGRADING index 2e4700c1c95..5d72233e904 100644 --- a/UPGRADING +++ b/UPGRADING @@ -100,6 +100,11 @@ PHP 8.1 UPGRADE NOTES - Curl: . Added CURLOPT_DOH_URL option. + . Added CURLStringFile, which can be used to post a file from a string rather + than a file: + + $file = new CURLStringFile($data, 'filename.txt', 'text/plain'); + curl_setopt($curl, CURLOPT_POSTFIELDS, ['file' => $file]); - hash: . The following functions have changed signatures: diff --git a/ext/curl/curl_file.c b/ext/curl/curl_file.c index a81239a60ab..a8f8a793b20 100644 --- a/ext/curl/curl_file.c +++ b/ext/curl/curl_file.c @@ -25,6 +25,7 @@ #include "curl_file_arginfo.h" PHP_CURL_API zend_class_entry *curl_CURLFile_class; +PHP_CURL_API zend_class_entry *curl_CURLStringFile_class; static void curlfile_ctor(INTERNAL_FUNCTION_PARAMETERS) { @@ -120,9 +121,34 @@ ZEND_METHOD(CURLFile, setPostFilename) } /* }}} */ +ZEND_METHOD(CURLStringFile, __construct) +{ + zend_string *data, *postname, *mime = NULL; + zval *object; + + object = ZEND_THIS; + + ZEND_PARSE_PARAMETERS_START(2,3) + Z_PARAM_STR(data) + Z_PARAM_STR(postname) + Z_PARAM_OPTIONAL + Z_PARAM_STR(mime) + ZEND_PARSE_PARAMETERS_END(); + + zend_update_property_str(curl_CURLStringFile_class, Z_OBJ_P(object), "data", sizeof("data") - 1, data); + zend_update_property_str(curl_CURLStringFile_class, Z_OBJ_P(object), "postname", sizeof("postname")-1, postname); + if (mime) { + zend_update_property_str(curl_CURLStringFile_class, Z_OBJ_P(object), "mime", sizeof("mime")-1, mime); + } else { + zend_update_property_string(curl_CURLStringFile_class, Z_OBJ_P(object), "mime", sizeof("mime")-1, "application/octet-stream"); + } +} + void curlfile_register_class(void) { curl_CURLFile_class = register_class_CURLFile(); curl_CURLFile_class->serialize = zend_class_serialize_deny; curl_CURLFile_class->unserialize = zend_class_unserialize_deny; + + curl_CURLStringFile_class = register_class_CURLStringFile(); } diff --git a/ext/curl/curl_file.stub.php b/ext/curl/curl_file.stub.php index ecc14969149..041b780b9a6 100644 --- a/ext/curl/curl_file.stub.php +++ b/ext/curl/curl_file.stub.php @@ -28,3 +28,12 @@ class CURLFile /** @return void */ public function setPostFilename(string $posted_filename) {} } + +class CURLStringFile +{ + public string $data; + public string $postname; + public string $mime; + + public function __construct(string $data, string $postname, string $mime = "application/octet-stream") {} +} diff --git a/ext/curl/curl_file_arginfo.h b/ext/curl/curl_file_arginfo.h index 10b7ad04d78..400f2fd5f8c 100644 --- a/ext/curl/curl_file_arginfo.h +++ b/ext/curl/curl_file_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 2bd78005380fd7f885618c4cb993bb21abe8cea9 */ + * Stub hash: fdeef1c2a9e835b443d6e4cced23656ce21d8a30 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_CURLFile___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0) @@ -22,6 +22,12 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_CURLFile_setPostFilename, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, posted_filename, IS_STRING, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_CURLStringFile___construct, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, postname, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, mime, IS_STRING, 0, "\"application/octet-stream\"") +ZEND_END_ARG_INFO() + ZEND_METHOD(CURLFile, __construct); ZEND_METHOD(CURLFile, getFilename); @@ -29,6 +35,7 @@ ZEND_METHOD(CURLFile, getMimeType); ZEND_METHOD(CURLFile, getPostFilename); ZEND_METHOD(CURLFile, setMimeType); ZEND_METHOD(CURLFile, setPostFilename); +ZEND_METHOD(CURLStringFile, __construct); static const zend_function_entry class_CURLFile_methods[] = { @@ -41,6 +48,12 @@ static const zend_function_entry class_CURLFile_methods[] = { ZEND_FE_END }; + +static const zend_function_entry class_CURLStringFile_methods[] = { + ZEND_ME(CURLStringFile, __construct, arginfo_class_CURLStringFile___construct, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + zend_class_entry *register_class_CURLFile() { zend_class_entry ce, *class_entry; @@ -69,3 +82,31 @@ zend_class_entry *register_class_CURLFile() return class_entry; } +zend_class_entry *register_class_CURLStringFile() +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "CURLStringFile", class_CURLStringFile_methods); + class_entry = zend_register_internal_class_ex(&ce, NULL); + + zval property_data_default_value; + ZVAL_UNDEF(&property_data_default_value); + zend_string *property_data_name = zend_string_init("data", sizeof("data") - 1, 1); + zend_declare_typed_property(class_entry, property_data_name, &property_data_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_data_name); + + zval property_postname_default_value; + ZVAL_UNDEF(&property_postname_default_value); + zend_string *property_postname_name = zend_string_init("postname", sizeof("postname") - 1, 1); + zend_declare_typed_property(class_entry, property_postname_name, &property_postname_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_postname_name); + + zval property_mime_default_value; + ZVAL_UNDEF(&property_mime_default_value); + zend_string *property_mime_name = zend_string_init("mime", sizeof("mime") - 1, 1); + zend_declare_typed_property(class_entry, property_mime_name, &property_mime_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_mime_name); + + return class_entry; +} + diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index 49201334b96..3c219d70144 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -90,6 +90,9 @@ struct _php_curl_send_headers { struct _php_curl_free { zend_llist post; zend_llist stream; +#if LIBCURL_VERSION_NUM < 0x073800 /* 7.56.0 */ + zend_llist buffers; +#endif HashTable *slist; }; diff --git a/ext/curl/interface.c b/ext/curl/interface.c index d96334cd07b..0827bdfd8b3 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -1654,6 +1654,15 @@ static void curl_free_cb_arg(void **cb_arg_p) } /* }}} */ +#if LIBCURL_VERSION_NUM < 0x073800 /* 7.56.0 */ +/* {{{ curl_free_buffers */ +static void curl_free_buffers(void **buffer) +{ + zend_string_release((zend_string *) *buffer); +} +/* }}} */ +#endif + /* {{{ curl_free_slist */ static void curl_free_slist(zval *el) { @@ -1744,6 +1753,10 @@ void init_curl_handle(php_curl *ch) zend_llist_init(&ch->to_free->post, sizeof(struct HttpPost *), (llist_dtor_func_t)curl_free_post, 0); zend_llist_init(&ch->to_free->stream, sizeof(struct mime_data_cb_arg *), (llist_dtor_func_t)curl_free_cb_arg, 0); +#if LIBCURL_VERSION_NUM < 0x073800 /* 7.56.0 */ + zend_llist_init(&ch->to_free->buffers, sizeof(zend_string *), (llist_dtor_func_t)curl_free_buffers, 0); +#endif + ch->to_free->slist = emalloc(sizeof(HashTable)); zend_hash_init(ch->to_free->slist, 4, NULL, curl_free_slist, 0); ZVAL_UNDEF(&ch->postfields); @@ -2086,6 +2099,78 @@ static inline int build_mime_structure_from_hash(php_curl *ch, zval *zpostfields continue; } + 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; + + prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "postname", sizeof("postname")-1, 0, &rv); + if (EG(exception)) { + zend_string_release_ex(string_key, 0); + return FAILURE; + } + ZVAL_DEREF(prop); + ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING); + + 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)) { + zend_string_release_ex(string_key, 0); + return FAILURE; + } + ZVAL_DEREF(prop); + ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING); + + type = Z_STRVAL_P(prop); + + prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "data", sizeof("data")-1, 0, &rv); + if (EG(exception)) { + zend_string_release_ex(string_key, 0); + return FAILURE; + } + ZVAL_DEREF(prop); + ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING); + + postval = Z_STR_P(prop); + +#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */ + zval_ptr_dtor(&ch->postfields); + ZVAL_COPY(&ch->postfields, zpostfields); + + part = curl_mime_addpart(mime); + if (part == NULL) { + 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 + || (form_error = curl_mime_filename(part, filename)) != CURLE_OK + || (form_error = curl_mime_type(part, type)) != CURLE_OK) { + error = form_error; + } +#else + postval = zend_string_copy(postval); + zend_llist_add_element(&ch->to_free->buffers, &postval); + + form_error = curl_formadd(&first, &last, + CURLFORM_COPYNAME, ZSTR_VAL(string_key), + CURLFORM_NAMELENGTH, ZSTR_LEN(string_key), + CURLFORM_BUFFER, filename, + CURLFORM_CONTENTTYPE, type, + CURLFORM_BUFFERPTR, ZSTR_VAL(postval), + CURLFORM_BUFFERLENGTH, 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_string_release_ex(string_key, 0); + continue; + } + postval = zval_get_tmp_string(current, &tmp_postval); #if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */ @@ -3330,6 +3415,11 @@ static void curl_free_obj(zend_object *object) if (--(*ch->clone) == 0) { zend_llist_clean(&ch->to_free->post); zend_llist_clean(&ch->to_free->stream); + +#if LIBCURL_VERSION_NUM < 0x073800 /* 7.56.0 */ + zend_llist_clean(&ch->to_free->buffers); +#endif + zend_hash_destroy(ch->to_free->slist); efree(ch->to_free->slist); efree(ch->to_free); diff --git a/ext/curl/php_curl.h b/ext/curl/php_curl.h index 88806262ece..09e20602f41 100644 --- a/ext/curl/php_curl.h +++ b/ext/curl/php_curl.h @@ -39,5 +39,6 @@ PHP_CURL_API extern zend_class_entry *curl_ce; PHP_CURL_API extern zend_class_entry *curl_share_ce; PHP_CURL_API extern zend_class_entry *curl_multi_ce; PHP_CURL_API extern zend_class_entry *curl_CURLFile_class; +PHP_CURL_API extern zend_class_entry *curl_CURLStringFile_class; #endif /* _PHP_CURL_H */ diff --git a/ext/curl/tests/curl_string_file_upload.phpt b/ext/curl/tests/curl_string_file_upload.phpt new file mode 100644 index 00000000000..65a041e1466 --- /dev/null +++ b/ext/curl/tests/curl_string_file_upload.phpt @@ -0,0 +1,86 @@ +--TEST-- +CURL file uploading from string +--SKIPIF-- + +--FILE-- + $file)); + var_dump(curl_exec($ch)); +} + +include 'server.inc'; +$host = curl_cli_server_start(); +$ch = curl_init(); +curl_setopt($ch, CURLOPT_URL, "{$host}/get.php?test=string_file"); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + +$data = "test\0test"; +var_dump(md5($data)); +testcurl($ch, 'foo.txt', $data); +testcurl($ch, 'foo.txt', $data, 'text/plain'); +testcurl($ch, '', $data); +testcurl($ch, 'foo.txt', ''); +testcurl($ch, "foo.txt\0broken_string", $data, "text/plain\0broken_string"); + +// properties +$file = new CURLStringFile($data, 'foo.txt'); +$file->mime = 'text/plain'; +var_dump($file->mime); +var_dump($file->postname); +var_dump(md5($file->data)); +curl_setopt($ch, CURLOPT_POSTFIELDS, array("file" => $file)); +var_dump(curl_exec($ch)); + +// serialization / deserialization +$old = new CURLStringFile($data, 'foo.txt', 'text/plain'); +$serialized = serialize($old); +$new = unserialize($serialized); +curl_setopt($ch, CURLOPT_POSTFIELDS, array("file" => $new)); +var_dump(curl_exec($ch)); + +// destroy object before send request +$file = new CURLStringFile($data, 'foo.txt', 'text/plain'); +curl_setopt($ch, CURLOPT_POSTFIELDS, array("file" => $file)); +unset($file); +var_dump(curl_exec($ch)); + +// clone curl handler +$file = new CURLStringFile($data, 'foo.txt', 'text/plain'); +curl_setopt($ch, CURLOPT_POSTFIELDS, array("file" => $file)); +$ch2 = clone $ch; +var_dump(curl_exec($ch2)); + +// properties are references + +$file = new CURLStringFile($data, 'foo.txt', 'text/plain'); +$data =& $file->data; +$postname =& $file->postname; +$mime =& $file->mime; +curl_setopt($ch, CURLOPT_POSTFIELDS, array("file" => $file)); +var_dump(curl_exec($ch)); + +?> +--EXPECTF-- +string(%d) "62942c05ed0d1b501c4afe6dc1c4db1b" +string(%d) "foo.txt|application/octet-stream|62942c05ed0d1b501c4afe6dc1c4db1b" +string(%d) "foo.txt|text/plain|62942c05ed0d1b501c4afe6dc1c4db1b" +string(%d) "error:4" +string(%d) "foo.txt|application/octet-stream|d41d8cd98f00b204e9800998ecf8427e" +string(%d) "foo.txt|text/plain|62942c05ed0d1b501c4afe6dc1c4db1b" +string(%d) "text/plain" +string(%d) "foo.txt" +string(%d) "62942c05ed0d1b501c4afe6dc1c4db1b" +string(%d) "foo.txt|text/plain|62942c05ed0d1b501c4afe6dc1c4db1b" +string(%d) "foo.txt|text/plain|62942c05ed0d1b501c4afe6dc1c4db1b" +string(%d) "foo.txt|text/plain|62942c05ed0d1b501c4afe6dc1c4db1b" +string(%d) "foo.txt|text/plain|62942c05ed0d1b501c4afe6dc1c4db1b" +string(%d) "foo.txt|text/plain|62942c05ed0d1b501c4afe6dc1c4db1b" diff --git a/ext/curl/tests/responder/get.inc b/ext/curl/tests/responder/get.inc index 64ab267d503..4ed9ae02823 100644 --- a/ext/curl/tests/responder/get.inc +++ b/ext/curl/tests/responder/get.inc @@ -31,6 +31,15 @@ echo $_FILES['file']['name'] . '|' . $_FILES['file']['type'] . '|' . $_FILES['file']['size']; } break; + case 'string_file': + if (isset($_FILES['file'])) { + if ($_FILES['file']['error'] === UPLOAD_ERR_OK) { + echo $_FILES['file']['name'] . '|' . $_FILES['file']['type'] . '|' . md5_file($_FILES['file']['tmp_name']); + } else { + echo 'error:' . $_FILES['file']['error']; + } + } + break; case 'method': echo $_SERVER['REQUEST_METHOD']; break;