Skip to content

Commit 88cf76e

Browse files
committed
Implement GH-19249: http context - allow content to be a stream/resource
This may be useful for sending large chunks of data.
1 parent 7b3e68f commit 88cf76e

File tree

5 files changed

+205
-14
lines changed

5 files changed

+205
-14
lines changed

UPGRADING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ PHP 8.5 UPGRADE NOTES
263263
and the function returns false. Previously, these errors were silently
264264
ignored. This change affects only the sendmail transport.
265265
. getimagesize() now supports HEIF/HEIC images.
266+
. The "http" stream context's "content" field now supports streams.
266267

267268
- Standard:
268269
. getimagesize() now supports SVG images when ext-libxml is also loaded.

ext/standard/http_fopen_wrapper.c

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,43 @@ static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *w
352352
return NULL;
353353
}
354354

355+
static bool php_stream_unwrap_content(php_stream_context *context, zend_string **str_out, php_stream **stream_out)
356+
{
357+
zval *content = php_stream_context_get_option(context, "http", "content");
358+
if (content) {
359+
if (Z_TYPE_P(content) == IS_STRING && Z_STRLEN_P(content) > 0) {
360+
*str_out = Z_STR_P(content);
361+
return true;
362+
} else if (Z_TYPE_P(content) == IS_RESOURCE) {
363+
if ((php_stream_from_zval_no_verify(*stream_out, content))) {
364+
return true;
365+
}
366+
}
367+
}
368+
369+
return false;
370+
}
371+
372+
static bool php_stream_append_content_length(smart_str *req_buf, zend_string *content_str, php_stream *content_stream)
373+
{
374+
smart_str_appends(req_buf, "Content-Length: ");
375+
if (content_str) {
376+
smart_str_append_unsigned(req_buf, ZSTR_LEN(content_str));
377+
} else {
378+
zend_off_t current_position = php_stream_tell(content_stream);
379+
if (php_stream_seek(content_stream, 0, SEEK_END) < 0) {
380+
return false;
381+
}
382+
zend_off_t end_position = php_stream_tell(content_stream);
383+
if (php_stream_seek(content_stream, current_position, SEEK_SET) < 0) {
384+
return false;
385+
}
386+
smart_str_append_unsigned(req_buf, end_position - current_position);
387+
}
388+
smart_str_appends(req_buf, "\r\n");
389+
return true;
390+
}
391+
355392
static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
356393
const char *path, const char *mode, int options, zend_string **opened_path,
357394
php_stream_context *context, int redirect_max, int flags,
@@ -832,6 +869,9 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
832869
}
833870
}
834871

872+
zend_string *content_str = NULL;
873+
php_stream *content_stream = NULL;
874+
835875
if (user_headers) {
836876
/* A bit weird, but some servers require that Content-Length be sent prior to Content-Type for POST
837877
* see bug #44603 for details. Since Content-Type maybe part of user's headers we need to do this check first.
@@ -840,42 +880,71 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
840880
(header_init || redirect_keep_method) &&
841881
context &&
842882
!(have_header & HTTP_HEADER_CONTENT_LENGTH) &&
843-
(tmpzval = php_stream_context_get_option(context, "http", "content")) != NULL &&
844-
Z_TYPE_P(tmpzval) == IS_STRING && Z_STRLEN_P(tmpzval) > 0
883+
php_stream_unwrap_content(context, &content_str, &content_stream)
845884
) {
846-
smart_str_appends(&req_buf, "Content-Length: ");
847-
smart_str_append_unsigned(&req_buf, Z_STRLEN_P(tmpzval));
848-
smart_str_appends(&req_buf, "\r\n");
885+
if (!php_stream_append_content_length(&req_buf, content_str, content_stream)) {
886+
php_stream_close(stream);
887+
stream = NULL;
888+
efree(user_headers);
889+
php_stream_wrapper_log_error(wrapper, options, "Unable to determine length of \"content\" stream!");
890+
goto out;
891+
}
849892
have_header |= HTTP_HEADER_CONTENT_LENGTH;
850893
}
851894

852895
smart_str_appends(&req_buf, user_headers);
853896
smart_str_appends(&req_buf, "\r\n");
854897
efree(user_headers);
898+
899+
/* php_stream_unwrap_content() may throw a TypeError for non-stream resources */
900+
if (UNEXPECTED(EG(exception))) {
901+
php_stream_close(stream);
902+
stream = NULL;
903+
goto out;
904+
}
855905
}
856906

857907
/* Request content, such as for POST requests */
858908
if ((header_init || redirect_keep_method) && context &&
859-
(tmpzval = php_stream_context_get_option(context, "http", "content")) != NULL &&
860-
Z_TYPE_P(tmpzval) == IS_STRING && Z_STRLEN_P(tmpzval) > 0) {
909+
(content_str || content_stream || php_stream_unwrap_content(context, &content_str, &content_stream))) {
861910
if (!(have_header & HTTP_HEADER_CONTENT_LENGTH)) {
862-
smart_str_appends(&req_buf, "Content-Length: ");
863-
smart_str_append_unsigned(&req_buf, Z_STRLEN_P(tmpzval));
864-
smart_str_appends(&req_buf, "\r\n");
911+
if (!php_stream_append_content_length(&req_buf, content_str, content_stream)) {
912+
php_stream_close(stream);
913+
stream = NULL;
914+
php_stream_wrapper_log_error(wrapper, options, "Unable to determine length of \"content\" stream!");
915+
goto out;
916+
}
865917
}
866918
if (!(have_header & HTTP_HEADER_TYPE)) {
867919
smart_str_appends(&req_buf, "Content-Type: application/x-www-form-urlencoded\r\n");
868920
php_error_docref(NULL, E_NOTICE, "Content-type not specified assuming application/x-www-form-urlencoded");
869921
}
870922
smart_str_appends(&req_buf, "\r\n");
871-
smart_str_appendl(&req_buf, Z_STRVAL_P(tmpzval), Z_STRLEN_P(tmpzval));
923+
if (content_str) {
924+
smart_str_append(&req_buf, content_str);
925+
php_stream_write(stream, ZSTR_VAL(req_buf.s), ZSTR_LEN(req_buf.s));
926+
} else {
927+
php_stream_write(stream, ZSTR_VAL(req_buf.s), ZSTR_LEN(req_buf.s));
928+
929+
if (SUCCESS != php_stream_copy_to_stream_ex(content_stream, stream, PHP_STREAM_COPY_ALL, NULL)) {
930+
php_stream_close(stream);
931+
stream = NULL;
932+
php_stream_wrapper_log_error(wrapper, options, "Unable to copy \"content\" stream!");
933+
goto out;
934+
}
935+
}
872936
} else {
937+
/* php_stream_unwrap_content() may throw a TypeError for non-stream resources */
938+
if (UNEXPECTED(EG(exception))) {
939+
php_stream_close(stream);
940+
stream = NULL;
941+
goto out;
942+
}
943+
873944
smart_str_appends(&req_buf, "\r\n");
945+
php_stream_write(stream, ZSTR_VAL(req_buf.s), ZSTR_LEN(req_buf.s));
874946
}
875947

876-
/* send it */
877-
php_stream_write(stream, ZSTR_VAL(req_buf.s), ZSTR_LEN(req_buf.s));
878-
879948
if (Z_ISUNDEF_P(response_header)) {
880949
array_init(response_header);
881950
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
GH-19249 (http context - allow content to be a stream/resource) - custom stream
3+
--INI--
4+
allow_url_fopen=1
5+
--CONFLICTS--
6+
server
7+
--FILE--
8+
<?php
9+
class MyStream {
10+
public $context;
11+
12+
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool {
13+
return true;
14+
}
15+
16+
public function stream_read(int $count): string|false {
17+
return false;
18+
}
19+
}
20+
21+
stream_wrapper_register("custom", MyStream::class);
22+
23+
$serverCode = <<<'CODE'
24+
var_dump(getallheaders()['Content-Length']);
25+
echo file_get_contents('php://input');
26+
CODE;
27+
28+
include __DIR__."/../../../../sapi/cli/tests/php_cli_server.inc";
29+
php_cli_server_start($serverCode, null, []);
30+
31+
$postData = fopen("custom://", "r");
32+
33+
echo file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS . "/", false, stream_context_create([
34+
'http' => [
35+
'method' => 'POST',
36+
'header' => [
37+
'Content-type: application/x-www-form-urlencoded',
38+
],
39+
'content' => $postData,
40+
]
41+
]));
42+
?>
43+
--EXPECTF--
44+
Warning: file_get_contents(): Stream does not support seeking in %s on line %d
45+
46+
Warning: file_get_contents(%s): Failed to open stream: Unable to determine length of "content" stream! in %s on line %d
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
GH-19249 (http context - allow content to be a stream/resource) - memory stream
3+
--INI--
4+
allow_url_fopen=1
5+
--CONFLICTS--
6+
server
7+
--FILE--
8+
<?php
9+
$serverCode = <<<'CODE'
10+
var_dump(getallheaders()['Content-Length']);
11+
echo file_get_contents('php://input');
12+
CODE;
13+
14+
include __DIR__."/../../../../sapi/cli/tests/php_cli_server.inc";
15+
php_cli_server_start($serverCode, null, []);
16+
17+
$postData = fopen("php://memory", "w+");
18+
fwrite($postData, "a=b&c=d");
19+
// Test skip
20+
fseek($postData, 4);
21+
22+
echo file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS . "/", false, stream_context_create([
23+
'http' => [
24+
'method' => 'POST',
25+
'header' => [
26+
'Content-type: application/x-www-form-urlencoded',
27+
],
28+
'content' => $postData,
29+
]
30+
]));
31+
?>
32+
--EXPECT--
33+
string(1) "3"
34+
c=d
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
GH-19249 (http context - allow content to be a stream/resource) - no stream
3+
--INI--
4+
allow_url_fopen=1
5+
--CONFLICTS--
6+
server
7+
--FILE--
8+
<?php
9+
$serverCode = '';
10+
11+
include __DIR__."/../../../../sapi/cli/tests/php_cli_server.inc";
12+
php_cli_server_start($serverCode, null, []);
13+
14+
$postData = proc_open("echo", [], $pipes);
15+
16+
$headers = [
17+
[],
18+
'header' => [
19+
'Content-type: application/x-www-form-urlencoded',
20+
],
21+
];
22+
23+
foreach ($headers as $header) {
24+
try {
25+
file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS . "/", false, stream_context_create([
26+
'http' => [
27+
'method' => 'POST',
28+
...$header,
29+
'content' => $postData,
30+
]
31+
]));
32+
} catch (TypeError $e) {
33+
echo $e->getMessage(), "\n";
34+
}
35+
}
36+
37+
proc_close($postData);
38+
?>
39+
--EXPECT--
40+
file_get_contents(): supplied resource is not a valid stream resource
41+
file_get_contents(): supplied resource is not a valid stream resource

0 commit comments

Comments
 (0)