Skip to content

Commit c68dc6b

Browse files
committed
Extend CURLFile to support streams
Due to former restrictions of the libcurl API, curl multipart/formdata file uploads supported only proper files. However, as of curl 7.56.0 the new `curl_mime_*()` API is available (and already supported by PHP[1]), which allows us to support arbitrary *seekable* streams, which is generally desirable, and particularly resolves issues with the transparent Unicode and long part support on Windows (see bug #77711). Note that older curl versions are still supported, but CURLFile is still restricted to proper files in this case. [1] <http://git.php.net/?p=php-src.git;a=commit;h=a83b68ba56714bfa06737a61af795460caa4a105>
1 parent 22e9a5e commit c68dc6b

File tree

7 files changed

+152
-1
lines changed

7 files changed

+152
-1
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ PHP NEWS
2020
- CURL:
2121
. Fixed bug #76480 (Use curl_multi_wait() so that timeouts are respected).
2222
(Pierrick)
23+
. Implemented FR #77711 (CURLFile should support UNICODE filenames). (cmb)
2324

2425
- Date:
2526
. Fixed bug #75232 (print_r of DateTime creating side-effect). (Nikita)

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ PHP 7.4 UPGRADE NOTES
129129
. Support for WeakReferences has been added.
130130
RFC: https://wiki.php.net/rfc/weakrefs
131131

132+
- CURL:
133+
. CURLFile now supports stream wrappers in addition to plain file names, if
134+
the extension has been built against libcurl >= 7.56.0. The streams may
135+
need to be seekable.
136+
132137
- Filter:
133138
. The FILTER_VALIDATE_FLOAT filter now supports the min_range and max_range
134139
options, with the same semantics as FILTER_VALIDATE_INT.

ext/curl/interface.c

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1803,6 +1803,14 @@ static void curl_free_post(void **post)
18031803
}
18041804
/* }}} */
18051805

1806+
/* {{{ curl_free_stream
1807+
*/
1808+
static void curl_free_stream(void **post)
1809+
{
1810+
php_stream_close((php_stream *)*post);
1811+
}
1812+
/* }}} */
1813+
18061814
/* {{{ curl_free_slist
18071815
*/
18081816
static void curl_free_slist(zval *el)
@@ -1894,6 +1902,7 @@ php_curl *alloc_curl_handle()
18941902

18951903
zend_llist_init(&ch->to_free->str, sizeof(char *), (llist_dtor_func_t)curl_free_string, 0);
18961904
zend_llist_init(&ch->to_free->post, sizeof(struct HttpPost *), (llist_dtor_func_t)curl_free_post, 0);
1905+
zend_llist_init(&ch->to_free->stream, sizeof(php_stream *), (llist_dtor_func_t)curl_free_stream, 0);
18971906

18981907
ch->to_free->slist = emalloc(sizeof(HashTable));
18991908
zend_hash_init(ch->to_free->slist, 4, NULL, curl_free_slist, 0);
@@ -2121,6 +2130,32 @@ PHP_FUNCTION(curl_copy_handle)
21212130
}
21222131
/* }}} */
21232132

2133+
#if LIBCURL_VERSION_NUM >= 0x073800
2134+
static size_t read_cb(char *buffer, size_t size, size_t nitems, void *arg) /* {{{ */
2135+
{
2136+
php_stream *stream = (php_stream *) arg;
2137+
size_t numread = php_stream_read(stream, buffer, nitems * size);
2138+
2139+
if (numread == (size_t)-1) {
2140+
return CURL_READFUNC_ABORT;
2141+
}
2142+
return numread;
2143+
}
2144+
/* }}} */
2145+
2146+
static int seek_cb(void *arg, curl_off_t offset, int origin) /* {{{ */
2147+
{
2148+
php_stream *stream = (php_stream *) arg;
2149+
int res = php_stream_seek(stream, offset, origin);
2150+
2151+
if (res) {
2152+
return CURL_SEEKFUNC_CANTSEEK;
2153+
}
2154+
return CURL_SEEKFUNC_OK;
2155+
}
2156+
/* }}} */
2157+
#endif
2158+
21242159
static int _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue) /* {{{ */
21252160
{
21262161
CURLcode error = CURLE_OK;
@@ -2756,6 +2791,9 @@ static int _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue) /* {{{
27562791
/* new-style file upload */
27572792
zval *prop, rv;
27582793
char *type = NULL, *filename = NULL;
2794+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2795+
php_stream *stream;
2796+
#endif
27592797

27602798
prop = zend_read_property(curl_CURLFile_class, current, "name", sizeof("name")-1, 0, &rv);
27612799
if (Z_TYPE_P(prop) != IS_STRING) {
@@ -2777,17 +2815,24 @@ static int _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue) /* {{{
27772815
}
27782816

27792817
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2818+
if (!(stream = php_stream_open_wrapper(ZSTR_VAL(postval), "rb", IGNORE_PATH, NULL))) {
2819+
zend_string_release_ex(string_key, 0);
2820+
return FAILURE;
2821+
}
27802822
part = curl_mime_addpart(mime);
27812823
if (part == NULL) {
2824+
php_stream_close(stream);
27822825
zend_string_release_ex(string_key, 0);
27832826
return FAILURE;
27842827
}
27852828
if ((form_error = curl_mime_name(part, ZSTR_VAL(string_key))) != CURLE_OK
2786-
|| (form_error = curl_mime_filedata(part, ZSTR_VAL(postval))) != CURLE_OK
2829+
|| (form_error = curl_mime_data_cb(part, -1, read_cb, seek_cb, NULL, stream)) != CURLE_OK
27872830
|| (form_error = curl_mime_filename(part, filename ? filename : ZSTR_VAL(postval))) != CURLE_OK
27882831
|| (form_error = curl_mime_type(part, type ? type : "application/octet-stream")) != CURLE_OK) {
2832+
php_stream_close(stream);
27892833
error = form_error;
27902834
}
2835+
zend_llist_add_element(&ch->to_free->stream, &stream);
27912836
#else
27922837
form_error = curl_formadd(&first, &last,
27932838
CURLFORM_COPYNAME, ZSTR_VAL(string_key),
@@ -3517,6 +3562,7 @@ static void _php_curl_close_ex(php_curl *ch)
35173562
if (--(*ch->clone) == 0) {
35183563
zend_llist_clean(&ch->to_free->str);
35193564
zend_llist_clean(&ch->to_free->post);
3565+
zend_llist_clean(&ch->to_free->stream);
35203566
zend_hash_destroy(ch->to_free->slist);
35213567
efree(ch->to_free->slist);
35223568
efree(ch->to_free);

ext/curl/php_curl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ struct _php_curl_send_headers {
167167
struct _php_curl_free {
168168
zend_llist str;
169169
zend_llist post;
170+
zend_llist stream;
170171
HashTable *slist;
171172
};
172173

ext/curl/tests/bug77711.phpt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
FR #77711 (CURLFile should support UNICODE filenames)
3+
--SKIPIF--
4+
<?php include 'skipif.inc'; ?>
5+
--FILE--
6+
<?php
7+
include 'server.inc';
8+
$host = curl_cli_server_start();
9+
10+
$ch = curl_init();
11+
curl_setopt($ch, CURLOPT_SAFE_UPLOAD, 1);
12+
curl_setopt($ch, CURLOPT_URL, "{$host}/get.php?test=file");
13+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
14+
15+
$filename = __DIR__ . '/АБВ.txt';
16+
file_put_contents($filename, "Test.");
17+
$file = curl_file_create($filename);
18+
$params = array('file' => $file);
19+
var_dump(curl_setopt($ch, CURLOPT_POSTFIELDS, $params));
20+
21+
var_dump(curl_exec($ch));
22+
curl_close($ch);
23+
?>
24+
===DONE===
25+
--EXPECTF--
26+
bool(true)
27+
string(%d) "АБВ.txt|application/octet-stream"
28+
===DONE===
29+
--CLEAN--
30+
<?php
31+
@unlink(__DIR__ . '/АБВ.txt');
32+
?>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
curl_copy_handle() allows to post CURLFile multiple times
3+
--SKIPIF--
4+
<?php include 'skipif.inc'; ?>
5+
--FILE--
6+
<?php
7+
include 'server.inc';
8+
$host = curl_cli_server_start();
9+
10+
$ch1 = curl_init();
11+
curl_setopt($ch1, CURLOPT_SAFE_UPLOAD, 1);
12+
curl_setopt($ch1, CURLOPT_URL, "{$host}/get.php?test=file");
13+
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, 1);
14+
15+
$filename = __DIR__ . '/АБВ.txt';
16+
file_put_contents($filename, "Test.");
17+
$file = curl_file_create($filename);
18+
$params = array('file' => $file);
19+
var_dump(curl_setopt($ch1, CURLOPT_POSTFIELDS, $params));
20+
21+
$ch2 = curl_copy_handle($ch1);
22+
23+
var_dump(curl_exec($ch1));
24+
curl_close($ch1);
25+
26+
var_dump(curl_exec($ch2));
27+
curl_close($ch2);
28+
?>
29+
===DONE===
30+
--EXPECTF--
31+
bool(true)
32+
string(%d) "АБВ.txt|application/octet-stream"
33+
string(%d) "АБВ.txt|application/octet-stream"
34+
===DONE===
35+
--CLEAN--
36+
<?php
37+
@unlink(__DIR__ . '/АБВ.txt');
38+
?>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
CURL file uploading from stream
3+
--SKIPIF--
4+
<?php include 'skipif.inc'; ?>
5+
<?php
6+
if (curl_version()['version_number'] < 0x73800) die('skip requires curl >= 7.56.0');
7+
--FILE--
8+
<?php
9+
include 'server.inc';
10+
$host = curl_cli_server_start();
11+
12+
$ch = curl_init();
13+
curl_setopt($ch, CURLOPT_SAFE_UPLOAD, 1);
14+
curl_setopt($ch, CURLOPT_URL, "{$host}/get.inc?test=file");
15+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
16+
17+
$file = curl_file_create('data://text/plain;base64,SSBsb3ZlIFBIUAo=', 'text/plain', 'i-love-php');
18+
$params = array('file' => $file);
19+
var_dump(curl_setopt($ch, CURLOPT_POSTFIELDS, $params));
20+
21+
var_dump(curl_exec($ch));
22+
curl_close($ch);
23+
?>
24+
===DONE===
25+
--EXPECT--
26+
bool(true)
27+
string(21) "i-love-php|text/plain"
28+
===DONE===

0 commit comments

Comments
 (0)