Skip to content

Commit 330b5e4

Browse files
committed
Fix GH-10229 http_build_query() skips Stringable params
The reason for this is that objects are treated like a key:value pair, like arrays, and called recursively Therefore, if the object is Stringable and does not have any visible propertiese we handle it like a scalar. Otherwise, we keep the existing key:value behaviour of recursing through the object properties.
1 parent 334ecbe commit 330b5e4

File tree

4 files changed

+175
-83
lines changed

4 files changed

+175
-83
lines changed

ext/standard/http.c

Lines changed: 152 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ static void php_url_encode_scalar(zval *scalar, smart_str *form_str,
6262
encoded_data = php_url_encode(Z_STRVAL_P(scalar), Z_STRLEN_P(scalar));
6363
}
6464
smart_str_append(form_str, encoded_data);
65-
zend_string_free(encoded_data);
65+
zend_string_release_ex(encoded_data, false);
6666
break;
6767
}
6868
case IS_LONG:
@@ -77,8 +77,8 @@ static void php_url_encode_scalar(zval *scalar, smart_str *form_str,
7777
encoded_data = php_url_encode(ZSTR_VAL(tmp), ZSTR_LEN(tmp));
7878
}
7979
smart_str_append(form_str, encoded_data);
80-
zend_string_free(tmp);
81-
zend_string_free(encoded_data);
80+
zend_string_release_ex(tmp, false);
81+
zend_string_release_ex(encoded_data, false);
8282
break;
8383
}
8484
case IS_FALSE:
@@ -92,11 +92,135 @@ static void php_url_encode_scalar(zval *scalar, smart_str *form_str,
9292
}
9393
}
9494

95+
static HashTable* php_object_get_visible_properties(/* const */ zend_object *object)
96+
{
97+
/* const */ HashTable *properties = object->handlers->get_properties(object);
98+
//HashTable *visible_properties = zend_new_array(zend_hash_num_elements(properties));
99+
HashTable *visible_properties = zend_array_dup(object->handlers->get_properties(object));
100+
zend_string *property_name;
101+
zval *value;
102+
103+
ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, value) {
104+
bool is_dynamic = true;
105+
if (Z_TYPE_P(value) == IS_INDIRECT) {
106+
value = Z_INDIRECT_P(value);
107+
if (Z_ISUNDEF_P(value)) {
108+
continue;
109+
}
110+
is_dynamic = false;
111+
}
112+
113+
if (zend_check_property_access(object, property_name, is_dynamic) == FAILURE) {
114+
/* property not visible in this scope */
115+
zend_hash_del(visible_properties, property_name);
116+
continue;
117+
}
118+
119+
/* handling for private & protected object properties */
120+
if (UNEXPECTED(ZSTR_VAL(property_name)[0] == '\0')) {
121+
zend_hash_del(visible_properties, property_name);
122+
const char *tmp;
123+
const char *unmangled_name;
124+
size_t unmangled_name_len;
125+
zend_unmangle_property_name_ex(property_name, &tmp, &unmangled_name, &unmangled_name_len);
126+
zend_hash_str_add(visible_properties, unmangled_name, unmangled_name_len, value);
127+
}
128+
} ZEND_HASH_FOREACH_END();
129+
return visible_properties;
130+
}
131+
132+
static zend_string* php_url_encode_get_new_prefix(
133+
int encoding_type, zend_ulong index_int,
134+
const char *index_string, size_t index_string_len,
135+
const char *num_prefix, size_t num_prefix_len,
136+
const zend_string *key_prefix
137+
) {
138+
zend_string *new_prefix;
139+
140+
if (index_string) {
141+
zend_string *encoded_key;
142+
if (encoding_type == PHP_QUERY_RFC3986) {
143+
encoded_key = php_raw_url_encode(index_string, index_string_len);
144+
} else {
145+
encoded_key = php_url_encode(index_string, index_string_len);
146+
}
147+
148+
if (key_prefix) {
149+
new_prefix = zend_string_concat3(ZSTR_VAL(key_prefix), ZSTR_LEN(key_prefix), ZSTR_VAL(encoded_key), ZSTR_LEN(encoded_key), "%5D%5B", strlen("%5D%5B"));
150+
} else {
151+
new_prefix = zend_string_concat2(ZSTR_VAL(encoded_key), ZSTR_LEN(encoded_key), "%5B", strlen("%5B"));
152+
}
153+
zend_string_release_ex(encoded_key, false);
154+
} else { /* is integer index */
155+
char *index_int_as_str;
156+
size_t index_int_as_str_len;
157+
158+
index_int_as_str_len = spprintf(&index_int_as_str, 0, ZEND_LONG_FMT, index_int);
159+
160+
if (key_prefix && num_prefix) {
161+
/* zend_string_concat4() */
162+
size_t len = ZSTR_LEN(key_prefix) + num_prefix_len + index_int_as_str_len + strlen("%5D%5B");
163+
new_prefix = zend_string_alloc(len, 0);
164+
165+
memcpy(ZSTR_VAL(new_prefix), ZSTR_VAL(key_prefix), ZSTR_LEN(key_prefix));
166+
memcpy(ZSTR_VAL(new_prefix) + ZSTR_LEN(key_prefix), num_prefix, num_prefix_len);
167+
memcpy(ZSTR_VAL(new_prefix) + ZSTR_LEN(key_prefix) + num_prefix_len, index_int_as_str, index_int_as_str_len);
168+
memcpy(ZSTR_VAL(new_prefix) + ZSTR_LEN(key_prefix) + num_prefix_len +index_int_as_str_len, "%5D%5B", strlen("%5D%5B"));
169+
ZSTR_VAL(new_prefix)[len] = '\0';
170+
} else if (key_prefix) {
171+
new_prefix = zend_string_concat3(ZSTR_VAL(key_prefix), ZSTR_LEN(key_prefix), index_int_as_str, index_int_as_str_len, "%5D%5B", strlen("%5D%5B"));
172+
} else if (num_prefix) {
173+
new_prefix = zend_string_concat3(num_prefix, num_prefix_len, index_int_as_str, index_int_as_str_len, "%5B", strlen("%5B"));
174+
} else {
175+
new_prefix = zend_string_concat2(index_int_as_str, index_int_as_str_len, "%5B", strlen("%5B"));
176+
}
177+
efree(index_int_as_str);
178+
}
179+
180+
return new_prefix;
181+
}
182+
183+
static void php_url_encode_object(zend_object *object, smart_str *form_str,
184+
int encoding_type, zend_ulong index_int,
185+
const char *index_string, size_t index_string_len,
186+
const char *num_prefix, size_t num_prefix_len,
187+
const zend_string *key_prefix,
188+
const zend_string *arg_sep)
189+
{
190+
if (GC_IS_RECURSIVE(object)) {
191+
/* Prevent recursion */
192+
return;
193+
}
194+
195+
HashTable *properties = php_object_get_visible_properties(object);
196+
if (zend_hash_num_elements(properties) == 0) {
197+
zend_array_destroy(properties);
198+
199+
zval tmp;
200+
/* If the data object is stringable without visible properties handle it like a string instead of empty array */
201+
if (object->handlers->cast_object(object, &tmp, IS_STRING) == SUCCESS) {
202+
php_url_encode_scalar(&tmp, form_str,
203+
encoding_type, index_int,
204+
index_string, index_string_len,
205+
num_prefix, num_prefix_len,
206+
NULL,
207+
arg_sep);
208+
zval_ptr_dtor(&tmp);
209+
}
210+
return;
211+
}
212+
213+
GC_TRY_PROTECT_RECURSION(object);
214+
php_url_encode_hash_ex(properties, form_str, num_prefix, num_prefix_len, key_prefix, NULL, arg_sep, encoding_type);
215+
GC_TRY_UNPROTECT_RECURSION(object);
216+
zend_array_destroy(properties);
217+
}
218+
95219
/* {{{ php_url_encode_hash */
96220
PHPAPI void php_url_encode_hash_ex(HashTable *ht, smart_str *formstr,
97221
const char *num_prefix, size_t num_prefix_len,
98222
const zend_string *key_prefix,
99-
zval *type, const zend_string *arg_sep, int enc_type)
223+
ZEND_ATTRIBUTE_UNUSED zval *type, const zend_string *arg_sep, int enc_type)
100224
{
101225
zend_string *key = NULL;
102226
const char *prop_name;
@@ -118,84 +242,32 @@ PHPAPI void php_url_encode_hash_ex(HashTable *ht, smart_str *formstr,
118242
}
119243

120244
ZEND_HASH_FOREACH_KEY_VAL(ht, idx, key, zdata) {
121-
bool is_dynamic = 1;
122-
if (Z_TYPE_P(zdata) == IS_INDIRECT) {
123-
zdata = Z_INDIRECT_P(zdata);
124-
if (Z_ISUNDEF_P(zdata)) {
125-
continue;
126-
}
127-
128-
is_dynamic = 0;
129-
}
130-
131-
/* handling for private & protected object properties */
132245
if (key) {
133246
prop_name = ZSTR_VAL(key);
134247
prop_len = ZSTR_LEN(key);
135-
136-
if (type != NULL && zend_check_property_access(Z_OBJ_P(type), key, is_dynamic) != SUCCESS) {
137-
/* property not visible in this scope */
138-
continue;
139-
}
140-
141-
if (ZSTR_VAL(key)[0] == '\0' && type != NULL) {
142-
const char *tmp;
143-
zend_unmangle_property_name_ex(key, &tmp, &prop_name, &prop_len);
144-
} else {
145-
prop_name = ZSTR_VAL(key);
146-
prop_len = ZSTR_LEN(key);
147-
}
148248
} else {
149249
prop_name = NULL;
150250
prop_len = 0;
151251
}
152252

153253
ZVAL_DEREF(zdata);
154-
if (Z_TYPE_P(zdata) == IS_ARRAY || Z_TYPE_P(zdata) == IS_OBJECT) {
155-
zend_string *new_prefix;
156-
if (key) {
157-
zend_string *encoded_key;
158-
if (enc_type == PHP_QUERY_RFC3986) {
159-
encoded_key = php_raw_url_encode(prop_name, prop_len);
160-
} else {
161-
encoded_key = php_url_encode(prop_name, prop_len);
162-
}
163-
164-
if (key_prefix) {
165-
new_prefix = zend_string_concat3(ZSTR_VAL(key_prefix), ZSTR_LEN(key_prefix), ZSTR_VAL(encoded_key), ZSTR_LEN(encoded_key), "%5D%5B", strlen("%5D%5B"));
166-
} else {
167-
new_prefix = zend_string_concat2(ZSTR_VAL(encoded_key), ZSTR_LEN(encoded_key), "%5B", strlen("%5B"));
168-
}
169-
zend_string_release_ex(encoded_key, false);
170-
} else { /* is integer index */
171-
char *index_int_as_str;
172-
size_t index_int_as_str_len;
173-
174-
index_int_as_str_len = spprintf(&index_int_as_str, 0, ZEND_LONG_FMT, idx);
175-
176-
if (key_prefix && num_prefix) {
177-
/* zend_string_concat4() */
178-
size_t len = ZSTR_LEN(key_prefix) + num_prefix_len + index_int_as_str_len + strlen("%5D%5B");
179-
new_prefix = zend_string_alloc(len, 0);
180-
181-
memcpy(ZSTR_VAL(new_prefix), ZSTR_VAL(key_prefix), ZSTR_LEN(key_prefix));
182-
memcpy(ZSTR_VAL(new_prefix) + ZSTR_LEN(key_prefix), num_prefix, num_prefix_len);
183-
memcpy(ZSTR_VAL(new_prefix) + ZSTR_LEN(key_prefix) + num_prefix_len, index_int_as_str, index_int_as_str_len);
184-
memcpy(ZSTR_VAL(new_prefix) + ZSTR_LEN(key_prefix) + num_prefix_len +index_int_as_str_len, "%5D%5B", strlen("%5D%5B"));
185-
ZSTR_VAL(new_prefix)[len] = '\0';
186-
} else if (key_prefix) {
187-
new_prefix = zend_string_concat3(ZSTR_VAL(key_prefix), ZSTR_LEN(key_prefix), index_int_as_str, index_int_as_str_len, "%5D%5B", strlen("%5D%5B"));
188-
} else if (num_prefix) {
189-
new_prefix = zend_string_concat3(num_prefix, num_prefix_len, index_int_as_str, index_int_as_str_len, "%5B", strlen("%5B"));
190-
} else {
191-
new_prefix = zend_string_concat2(index_int_as_str, index_int_as_str_len, "%5B", strlen("%5B"));
192-
}
193-
efree(index_int_as_str);
194-
}
254+
if (Z_TYPE_P(zdata) == IS_ARRAY) {
255+
zend_string *new_prefix = php_url_encode_get_new_prefix(enc_type, idx, prop_name, prop_len,
256+
num_prefix, num_prefix_len, key_prefix);
195257
GC_TRY_PROTECT_RECURSION(ht);
196-
php_url_encode_hash_ex(HASH_OF(zdata), formstr, NULL, 0, new_prefix, (Z_TYPE_P(zdata) == IS_OBJECT ? zdata : NULL), arg_sep, enc_type);
258+
php_url_encode_hash_ex(Z_ARRVAL_P(zdata), formstr, NULL, 0, new_prefix, NULL, arg_sep, enc_type);
197259
GC_TRY_UNPROTECT_RECURSION(ht);
198260
zend_string_release_ex(new_prefix, false);
261+
} else if (Z_TYPE_P(zdata) == IS_OBJECT) {
262+
zend_string *new_prefix = php_url_encode_get_new_prefix(enc_type, idx, prop_name, prop_len,
263+
num_prefix, num_prefix_len, key_prefix);
264+
php_url_encode_object(Z_OBJ_P(zdata), formstr,
265+
enc_type, idx,
266+
prop_name, prop_len,
267+
num_prefix, num_prefix_len,
268+
new_prefix,
269+
arg_sep);
270+
zend_string_release_ex(new_prefix, false);
199271
} else if (Z_TYPE_P(zdata) == IS_NULL || Z_TYPE_P(zdata) == IS_RESOURCE) {
200272
/* Skip these types */
201273
continue;
@@ -230,7 +302,16 @@ PHP_FUNCTION(http_build_query)
230302
Z_PARAM_LONG(enc_type)
231303
ZEND_PARSE_PARAMETERS_END();
232304

233-
php_url_encode_hash_ex(HASH_OF(formdata), &formstr, prefix, prefix_len, /* key_prefix */ NULL, (Z_TYPE_P(formdata) == IS_OBJECT ? formdata : NULL), arg_sep, (int)enc_type);
305+
if (Z_TYPE_P(formdata) == IS_OBJECT) {
306+
php_url_encode_object(Z_OBJ_P(formdata), &formstr,
307+
(int) enc_type, /* int_index */ 0,
308+
/* string_index */ NULL, 0,
309+
prefix, prefix_len,
310+
NULL,
311+
arg_sep);
312+
} else {
313+
php_url_encode_hash_ex(Z_ARRVAL_P(formdata), &formstr, prefix, prefix_len, /* key_prefix */ NULL, NULL, arg_sep, (int)enc_type);
314+
}
234315

235316
RETURN_STR(smart_str_extract(&formstr));
236317
}

ext/standard/tests/http/http_build_query/bug26817.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ $obj->foo();
2222
var_dump(http_build_query($obj));
2323
?>
2424
--EXPECT--
25-
string(27) "foo=lala&bar=meuh&test=test"
25+
string(27) "test=test&foo=lala&bar=meuh"
2626
string(9) "test=test"

ext/standard/tests/http/http_build_query/http_build_query_object_just_stringable.phpt

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,30 @@ class StringableObject {
1010

1111
$o = new StringableObject();
1212

13+
var_dump(http_build_query(['hello', $o]));
14+
var_dump(http_build_query($o));
15+
var_dump(http_build_query(['hello', $o], numeric_prefix: 'prefix_'));
16+
var_dump(http_build_query($o, numeric_prefix: 'prefix_'));
17+
18+
class StringableObjectWithNonInternedString {
19+
public function __toString() : string {
20+
return str_repeat("abcd", 3);
21+
}
22+
}
23+
24+
$o = new StringableObjectWithNonInternedString();
25+
1326
var_dump(http_build_query(['hello', $o]));
1427
var_dump(http_build_query($o));
1528
var_dump(http_build_query(['hello', $o], numeric_prefix: 'prefix_'));
1629
var_dump(http_build_query($o, numeric_prefix: 'prefix_'));
1730
?>
1831
--EXPECT--
19-
string(7) "0=hello"
20-
string(0) ""
21-
string(14) "prefix_0=hello"
22-
string(0) ""
32+
string(20) "0=hello&1=Stringable"
33+
string(12) "0=Stringable"
34+
string(34) "prefix_0=hello&prefix_1=Stringable"
35+
string(19) "prefix_0=Stringable"
36+
string(22) "0=hello&1=abcdabcdabcd"
37+
string(14) "0=abcdabcdabcd"
38+
string(36) "prefix_0=hello&prefix_1=abcdabcdabcd"
39+
string(21) "prefix_0=abcdabcdabcd"

run-tests.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -857,13 +857,7 @@ function write_information(): void
857857
$php_cgi_info = '';
858858
}
859859

860-
if ($phpdbg) {
861-
$phpdbg_info = shell_exec("$phpdbg $pass_options $info_params $no_file_cache -qrr \"$info_file\"");
862-
$php_info_sep = "\n---------------------------------------------------------------------";
863-
$phpdbg_info = "$php_info_sep\nPHP : $phpdbg $phpdbg_info$php_info_sep";
864-
} else {
865-
$phpdbg_info = '';
866-
}
860+
$phpdbg_info = '';
867861

868862
if (function_exists('opcache_invalidate')) {
869863
opcache_invalidate($info_file, true);

0 commit comments

Comments
 (0)