Skip to content

Commit fcc159b

Browse files
committed
Fix GH-20374: PHP with tidy and custom-tags
Both enums and integers map to TidyInteger, however, in the TidyInteger case we always used zval_get_long(). So for a non-numeric string, this would get turned into 0. 0 is the first enum value in that case, so the wrong enum value could be selected. To solve this, add special handling for strings and (stringable) objects such that we can explicitly check for numeric strings, and if they're not, handle them as normal strings instead of as 0. Closes GH-20387.
1 parent 0432395 commit fcc159b

File tree

3 files changed

+202
-4
lines changed

3 files changed

+202
-4
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.3.29
44

5+
- Tidy:
6+
. Fixed bug GH-20374 (PHP with tidy and custom-tags). (ndossche)
57

68
20 Nov 2025, PHP 8.3.28
79

ext/tidy/tests/gh20374.phpt

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
--TEST--
2+
GH-20374 (PHP with tidy and custom-tags)
3+
--EXTENSIONS--
4+
tidy
5+
--CREDITS--
6+
franck-paul
7+
--FILE--
8+
<?php
9+
10+
class MyStringable {
11+
public function __construct(private $ret) {}
12+
13+
public function __toString(): string {
14+
return $this->ret;
15+
}
16+
}
17+
18+
class MyThrowingStringable {
19+
public function __toString(): string {
20+
throw new Error('no');
21+
}
22+
}
23+
24+
$values = [
25+
'string blocklevel' => 'blocklevel',
26+
'int' => 1,
27+
'double overflow' => (string) (2.0**80.0),
28+
'numeric string int 1' => '1',
29+
'numeric string double 1.0' => '1.0',
30+
'false' => false,
31+
'true' => true,
32+
'NAN' => NAN,
33+
'INF' => INF,
34+
'object with numeric string int 0' => new MyStringable('0'),
35+
'object with string blocklevel' => new MyStringable('blocklevel'),
36+
'object with string empty' => new MyStringable('empty'),
37+
'object with exception' => new MyThrowingStringable,
38+
];
39+
40+
foreach ($values as $key => $value) {
41+
echo "--- $key ---\n";
42+
$str = '<custom-html-element>test</custom-html-element>';
43+
44+
$config = [
45+
'custom-tags' => $value,
46+
];
47+
48+
$tidy = new tidy();
49+
try {
50+
$tidy->parseString($str, $config, 'utf8');
51+
echo $tidy->value, "\n";
52+
} catch (Throwable $e) {
53+
echo $e::class, ": ", $e->getMessage(), "\n";
54+
}
55+
}
56+
57+
?>
58+
--EXPECT--
59+
--- string blocklevel ---
60+
<html>
61+
<head>
62+
<title></title>
63+
</head>
64+
<body>
65+
<custom-html-element>test</custom-html-element>
66+
</body>
67+
</html>
68+
--- int ---
69+
<html>
70+
<head>
71+
<title></title>
72+
</head>
73+
<body>
74+
<custom-html-element>test</custom-html-element>
75+
</body>
76+
</html>
77+
--- double overflow ---
78+
<html>
79+
<head>
80+
<title></title>
81+
</head>
82+
<body>
83+
test
84+
</body>
85+
</html>
86+
--- numeric string int 1 ---
87+
<html>
88+
<head>
89+
<title></title>
90+
</head>
91+
<body>
92+
<custom-html-element>test</custom-html-element>
93+
</body>
94+
</html>
95+
--- numeric string double 1.0 ---
96+
<html>
97+
<head>
98+
<title></title>
99+
</head>
100+
<body>
101+
<custom-html-element>test</custom-html-element>
102+
</body>
103+
</html>
104+
--- false ---
105+
<html>
106+
<head>
107+
<title></title>
108+
</head>
109+
<body>
110+
test
111+
</body>
112+
</html>
113+
--- true ---
114+
<html>
115+
<head>
116+
<title></title>
117+
</head>
118+
<body>
119+
<custom-html-element>test</custom-html-element>
120+
</body>
121+
</html>
122+
--- NAN ---
123+
<html>
124+
<head>
125+
<title></title>
126+
</head>
127+
<body>
128+
test
129+
</body>
130+
</html>
131+
--- INF ---
132+
<html>
133+
<head>
134+
<title></title>
135+
</head>
136+
<body>
137+
test
138+
</body>
139+
</html>
140+
--- object with numeric string int 0 ---
141+
<html>
142+
<head>
143+
<title></title>
144+
</head>
145+
<body>
146+
test
147+
</body>
148+
</html>
149+
--- object with string blocklevel ---
150+
<html>
151+
<head>
152+
<title></title>
153+
</head>
154+
<body>
155+
<custom-html-element>test</custom-html-element>
156+
</body>
157+
</html>
158+
--- object with string empty ---
159+
<custom-html-element>
160+
<html>
161+
<head>
162+
<title></title>
163+
</head>
164+
<body>
165+
test
166+
</body>
167+
</html>
168+
--- object with exception ---
169+
Error: no

ext/tidy/tidy.c

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,37 @@ static int _php_tidy_set_tidy_opt(TidyDoc doc, char *optname, zval *value)
251251
zend_tmp_string_release(tmp_str);
252252
break;
253253

254-
case TidyInteger:
255-
lval = zval_get_long(value);
256-
if (tidyOptSetInt(doc, tidyOptGetId(opt), lval)) {
257-
return SUCCESS;
254+
case TidyInteger: /* integer or enum */
255+
ZVAL_DEREF(value);
256+
/* Enum will correspond to a non-numeric string or object */
257+
if (Z_TYPE_P(value) == IS_STRING || Z_TYPE_P(value) == IS_OBJECT) {
258+
double dval;
259+
str = zval_try_get_tmp_string(value, &tmp_str);
260+
if (UNEXPECTED(!str)) {
261+
return FAILURE;
262+
}
263+
uint8_t type = is_numeric_string(ZSTR_VAL(str), ZSTR_LEN(str), &lval, &dval, true);
264+
if (type == IS_DOUBLE) {
265+
lval = zend_dval_to_lval_cap(dval);
266+
type = IS_LONG;
267+
}
268+
if (type == IS_LONG) {
269+
if (tidyOptSetInt(doc, tidyOptGetId(opt), lval)) {
270+
zend_tmp_string_release(tmp_str);
271+
return SUCCESS;
272+
}
273+
} else {
274+
if (tidyOptSetValue(doc, tidyOptGetId(opt), ZSTR_VAL(str))) {
275+
zend_tmp_string_release(tmp_str);
276+
return SUCCESS;
277+
}
278+
}
279+
zend_tmp_string_release(tmp_str);
280+
} else {
281+
lval = zval_get_long(value);
282+
if (tidyOptSetInt(doc, tidyOptGetId(opt), lval)) {
283+
return SUCCESS;
284+
}
258285
}
259286
break;
260287

0 commit comments

Comments
 (0)