Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 969d2c3

Browse files
committedJan 19, 2025
Add XMLDocument::createFromStream()
1 parent de141f2 commit 969d2c3

File tree

7 files changed

+202
-58
lines changed

7 files changed

+202
-58
lines changed
 

‎ext/dom/document.c

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,7 +1345,17 @@ const char *dom_get_valid_file_path(const char *source, char *resolved_path, int
13451345
}
13461346
/* }}} */
13471347

1348-
xmlDocPtr dom_document_parser(zval *id, dom_load_mode mode, const char *source, size_t source_len, size_t options, xmlCharEncodingHandlerPtr encoding) /* {{{ */
1348+
static int dom_stream_read(void *context, char *buffer, int len)
1349+
{
1350+
zend_resource *resource = context;
1351+
if (EXPECTED(resource->ptr)) {
1352+
php_stream *stream = resource->ptr;
1353+
return php_stream_read(stream, buffer, len);
1354+
}
1355+
return -1;
1356+
}
1357+
1358+
xmlDocPtr dom_document_parser(zval *id, dom_load_mode mode, dom_source_union source, size_t options, xmlCharEncodingHandlerPtr encoding, const char *override_document_uri) /* {{{ */
13491359
{
13501360
xmlDocPtr ret;
13511361
xmlParserCtxtPtr ctxt = NULL;
@@ -1371,16 +1381,18 @@ xmlDocPtr dom_document_parser(zval *id, dom_load_mode mode, const char *source,
13711381
xmlInitParser();
13721382

13731383
if (mode == DOM_LOAD_FILE) {
1374-
if (CHECK_NULL_PATH(source, source_len)) {
1384+
if (CHECK_NULL_PATH(source.str, source.str_len)) {
13751385
zend_argument_value_error(1, "must not contain any null bytes");
13761386
return NULL;
13771387
}
1378-
const char *file_dest = dom_get_valid_file_path(source, resolved_path, MAXPATHLEN);
1388+
const char *file_dest = dom_get_valid_file_path(source.str, resolved_path, MAXPATHLEN);
13791389
if (file_dest) {
13801390
ctxt = xmlCreateFileParserCtxt(file_dest);
13811391
}
1392+
} else if (mode == DOM_LOAD_STRING) {
1393+
ctxt = xmlCreateMemoryParserCtxt(source.str, source.str_len);
13821394
} else {
1383-
ctxt = xmlCreateMemoryParserCtxt(source, source_len);
1395+
ctxt = xmlCreateIOParserCtxt(NULL, NULL, dom_stream_read, NULL, source.stream->res, XML_CHAR_ENCODING_NONE);
13841396
}
13851397

13861398
if (ctxt == NULL) {
@@ -1393,7 +1405,7 @@ xmlDocPtr dom_document_parser(zval *id, dom_load_mode mode, const char *source,
13931405
}
13941406

13951407
/* If loading from memory, we need to set the base directory for the document */
1396-
if (mode != DOM_LOAD_FILE) {
1408+
if (mode == DOM_LOAD_STRING) {
13971409
#ifdef HAVE_GETCWD
13981410
directory = VCWD_GETCWD(resolved_path, MAXPATHLEN);
13991411
#elif defined(HAVE_GETWD)
@@ -1410,6 +1422,8 @@ xmlDocPtr dom_document_parser(zval *id, dom_load_mode mode, const char *source,
14101422
}
14111423
ctxt->directory = (char *) xmlCanonicPath((const xmlChar *) resolved_path);
14121424
}
1425+
} else if (override_document_uri) {
1426+
ctxt->directory = (char *) xmlCanonicPath((const xmlChar *) override_document_uri);
14131427
}
14141428

14151429
ctxt->vctxt.error = php_libxml_ctx_error;
@@ -1507,21 +1521,20 @@ static void php_dom_finish_loading_document(zval *this, zval *return_value, xmlD
15071521
RETURN_TRUE;
15081522
}
15091523

1510-
static void dom_parse_document(INTERNAL_FUNCTION_PARAMETERS, int mode)
1524+
static void dom_legacy_parse_document(INTERNAL_FUNCTION_PARAMETERS, int mode)
15111525
{
1512-
char *source;
1513-
size_t source_len;
1526+
dom_source_union source;
15141527
zend_long options = 0;
15151528

1516-
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|l", &source, &source_len, &options) == FAILURE) {
1529+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|l", &source.str, &source.str_len, &options) == FAILURE) {
15171530
RETURN_THROWS();
15181531
}
15191532

1520-
if (!source_len) {
1533+
if (!source.str_len) {
15211534
zend_argument_must_not_be_empty_error(1);
15221535
RETURN_THROWS();
15231536
}
1524-
if (ZEND_SIZE_T_INT_OVFL(source_len)) {
1537+
if (ZEND_SIZE_T_INT_OVFL(source.str_len)) {
15251538
php_error_docref(NULL, E_WARNING, "Input string is too long");
15261539
RETURN_FALSE;
15271540
}
@@ -1530,7 +1543,7 @@ static void dom_parse_document(INTERNAL_FUNCTION_PARAMETERS, int mode)
15301543
RETURN_FALSE;
15311544
}
15321545

1533-
xmlDocPtr newdoc = dom_document_parser(ZEND_THIS, mode, source, source_len, options, NULL);
1546+
xmlDocPtr newdoc = dom_document_parser(ZEND_THIS, mode, source, options, NULL, NULL);
15341547
if (newdoc == DOM_DOCUMENT_MALFORMED) {
15351548
newdoc = NULL;
15361549
}
@@ -1542,7 +1555,7 @@ Since: DOM Level 3
15421555
*/
15431556
PHP_METHOD(DOMDocument, load)
15441557
{
1545-
dom_parse_document(INTERNAL_FUNCTION_PARAM_PASSTHRU, DOM_LOAD_FILE);
1558+
dom_legacy_parse_document(INTERNAL_FUNCTION_PARAM_PASSTHRU, DOM_LOAD_FILE);
15461559
}
15471560
/* }}} end dom_document_load */
15481561

@@ -1551,7 +1564,7 @@ Since: DOM Level 3
15511564
*/
15521565
PHP_METHOD(DOMDocument, loadXML)
15531566
{
1554-
dom_parse_document(INTERNAL_FUNCTION_PARAM_PASSTHRU, DOM_LOAD_STRING);
1567+
dom_legacy_parse_document(INTERNAL_FUNCTION_PARAM_PASSTHRU, DOM_LOAD_STRING);
15551568
}
15561569
/* }}} end dom_document_loadxml */
15571570

‎ext/dom/php_dom.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,22 @@ void dom_set_document_ref_pointers(xmlNodePtr node, php_libxml_ref_obj *document
179179
void dom_set_document_ref_pointers_attr(xmlAttrPtr attr, php_libxml_ref_obj *document);
180180

181181
typedef enum {
182-
DOM_LOAD_STRING = 0,
183-
DOM_LOAD_FILE = 1,
182+
DOM_LOAD_STRING,
183+
DOM_LOAD_FILE,
184+
DOM_LOAD_STREAM,
184185
} dom_load_mode;
185186

187+
typedef union {
188+
struct {
189+
const char *str;
190+
size_t str_len;
191+
};
192+
php_stream *stream;
193+
} dom_source_union;
194+
186195
#define DOM_DOCUMENT_MALFORMED ((xmlDocPtr) -1)
187196

188-
xmlDocPtr dom_document_parser(zval *id, dom_load_mode mode, const char *source, size_t source_len, size_t options, xmlCharEncodingHandlerPtr encoding);
197+
xmlDocPtr dom_document_parser(zval *id, dom_load_mode mode, dom_source_union source, size_t options, xmlCharEncodingHandlerPtr encoding, const char *override_document_uri);
189198

190199
/* parentnode */
191200
void dom_parent_node_prepend(dom_object *context, zval *nodes, uint32_t nodesc);

‎ext/dom/php_dom.stub.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,6 +2068,9 @@ public static function createEmpty(string $version = "1.0", string $encoding = "
20682068

20692069
public static function createFromFile(string $path, int $options = 0, ?string $overrideEncoding = null): XMLDocument {}
20702070

2071+
/** @param resource $stream */
2072+
public static function createFromStream($stream, ?string $documentURI = null, int $options = 0, ?string $overrideEncoding = null): XMLDocument {}
2073+
20712074
public static function createFromString(string $source, int $options = 0, ?string $overrideEncoding = null): XMLDocument {}
20722075

20732076
/**

‎ext/dom/php_dom_arginfo.h

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
Dom\XMLDocument::createFromStream() - from memory
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$tmp = fopen("php://memory", "r+");
9+
fwrite($tmp, "<root/>");
10+
rewind($tmp);
11+
$dom1 = Dom\XMLDocument::createFromStream($tmp);
12+
rewind($tmp);
13+
$dom2 = Dom\XMLDocument::createFromStream($tmp, "http://example.com");
14+
fclose($tmp);
15+
16+
var_dump($dom1->documentURI);
17+
var_dump($dom2->documentURI);
18+
19+
echo $dom1->saveXml(), "\n";
20+
echo $dom2->saveXml(), "\n";
21+
22+
?>
23+
--EXPECT--
24+
string(11) "about:blank"
25+
string(18) "http://example.com"
26+
<?xml version="1.0" encoding="UTF-8"?>
27+
<root/>
28+
<?xml version="1.0" encoding="UTF-8"?>
29+
<root/>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
Dom\HTMLDocument::createFromStream() - broken stream
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
class MyStream {
9+
public $context;
10+
private bool $first = true;
11+
12+
public function stream_read(int $count): string|false {
13+
if ($this->first) {
14+
$this->first = false;
15+
return "<root><child>";
16+
}
17+
throw new Error("broken");
18+
}
19+
20+
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path) {
21+
return true;
22+
}
23+
24+
public function stream_close(): void {
25+
}
26+
27+
public function stream_eof(): bool {
28+
return !$this->first;
29+
}
30+
}
31+
32+
stream_wrapper_register("foo", MyStream::class);
33+
34+
$tmp = fopen("foo://", "r+");
35+
try {
36+
$dom = Dom\XMLDocument::createFromStream($tmp);
37+
} catch (Error $e) {
38+
echo $e->getMessage(), "\n";
39+
}
40+
fclose($tmp);
41+
42+
?>
43+
--EXPECT--
44+
broken

‎ext/dom/xml_document.c

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -131,43 +131,8 @@ PHP_METHOD(Dom_XMLDocument, createEmpty)
131131
RETURN_THROWS();
132132
}
133133

134-
static void load_from_helper(INTERNAL_FUNCTION_PARAMETERS, int mode)
134+
static void load_from_helper(zval *return_value, int mode, dom_source_union source, size_t options, const char *override_encoding, const char *override_document_uri)
135135
{
136-
const char *source, *override_encoding = NULL;
137-
size_t source_len, override_encoding_len;
138-
zend_long options = 0;
139-
if (zend_parse_parameters(
140-
ZEND_NUM_ARGS(),
141-
"s|lp!",
142-
&source,
143-
&source_len,
144-
&options,
145-
&override_encoding,
146-
&override_encoding_len
147-
) == FAILURE) {
148-
RETURN_THROWS();
149-
}
150-
151-
if (!source_len) {
152-
zend_argument_value_error(1, "must not be empty");
153-
RETURN_THROWS();
154-
}
155-
156-
if (ZEND_SIZE_T_INT_OVFL(source_len)) {
157-
zend_argument_value_error(1, "is too long");
158-
RETURN_THROWS();
159-
}
160-
161-
/* See php_libxml_streams_IO_open_wrapper(), apparently this caused issues in the past. */
162-
if (mode == DOM_LOAD_FILE && strstr(source, "%00")) {
163-
zend_argument_value_error(1, "must not contain percent-encoded NUL bytes");
164-
RETURN_THROWS();
165-
}
166-
167-
if (!check_options_validity(2, options)) {
168-
RETURN_THROWS();
169-
}
170-
171136
xmlCharEncodingHandlerPtr encoding = NULL;
172137
if (override_encoding != NULL) {
173138
encoding = xmlFindCharEncodingHandler(override_encoding);
@@ -178,14 +143,14 @@ static void load_from_helper(INTERNAL_FUNCTION_PARAMETERS, int mode)
178143
options |= XML_PARSE_IGNORE_ENC;
179144
}
180145

181-
xmlDocPtr lxml_doc = dom_document_parser(NULL, mode, source, source_len, options, encoding);
146+
xmlDocPtr lxml_doc = dom_document_parser(NULL, mode, source, options, encoding, override_document_uri);
182147
if (UNEXPECTED(lxml_doc == NULL || lxml_doc == DOM_DOCUMENT_MALFORMED)) {
183148
if (!EG(exception)) {
184149
if (lxml_doc == DOM_DOCUMENT_MALFORMED) {
185150
php_dom_throw_error_with_message(SYNTAX_ERR, "XML fragment is not well-formed", true);
186151
} else {
187152
if (mode == DOM_LOAD_FILE) {
188-
zend_throw_exception_ex(NULL, 0, "Cannot open file '%s'", source);
153+
zend_throw_exception_ex(NULL, 0, "Cannot open file '%s'", source.str);
189154
} else {
190155
php_dom_throw_error(INVALID_STATE_ERR, true);
191156
}
@@ -235,6 +200,49 @@ static void load_from_helper(INTERNAL_FUNCTION_PARAMETERS, int mode)
235200
dom_document_convert_to_modern(intern->document, lxml_doc);
236201
}
237202

203+
static void load_from_string_or_file_helper(INTERNAL_FUNCTION_PARAMETERS, int mode)
204+
{
205+
const char *source, *override_encoding = NULL;
206+
size_t source_len, override_encoding_len;
207+
zend_long options = 0;
208+
if (zend_parse_parameters(
209+
ZEND_NUM_ARGS(),
210+
"s|lp!",
211+
&source,
212+
&source_len,
213+
&options,
214+
&override_encoding,
215+
&override_encoding_len
216+
) == FAILURE) {
217+
RETURN_THROWS();
218+
}
219+
220+
if (!source_len) {
221+
zend_argument_value_error(1, "must not be empty");
222+
RETURN_THROWS();
223+
}
224+
225+
if (ZEND_SIZE_T_INT_OVFL(source_len)) {
226+
zend_argument_value_error(1, "is too long");
227+
RETURN_THROWS();
228+
}
229+
230+
/* See php_libxml_streams_IO_open_wrapper(), apparently this caused issues in the past. */
231+
if (mode == DOM_LOAD_FILE && strstr(source, "%00")) {
232+
zend_argument_value_error(1, "must not contain percent-encoded NUL bytes");
233+
RETURN_THROWS();
234+
}
235+
236+
if (!check_options_validity(2, options)) {
237+
RETURN_THROWS();
238+
}
239+
240+
dom_source_union source_union;
241+
source_union.str = source;
242+
source_union.str_len = source_len;
243+
load_from_helper(return_value, mode, source_union, options, override_encoding, NULL);
244+
}
245+
238246
void dom_document_convert_to_modern(php_libxml_ref_obj *document, xmlDocPtr lxml_doc)
239247
{
240248
php_dom_private_data *private_data = php_dom_private_data_create();
@@ -245,12 +253,41 @@ void dom_document_convert_to_modern(php_libxml_ref_obj *document, xmlDocPtr lxml
245253

246254
PHP_METHOD(Dom_XMLDocument, createFromString)
247255
{
248-
load_from_helper(INTERNAL_FUNCTION_PARAM_PASSTHRU, DOM_LOAD_STRING);
256+
load_from_string_or_file_helper(INTERNAL_FUNCTION_PARAM_PASSTHRU, DOM_LOAD_STRING);
249257
}
250258

251259
PHP_METHOD(Dom_XMLDocument, createFromFile)
252260
{
253-
load_from_helper(INTERNAL_FUNCTION_PARAM_PASSTHRU, DOM_LOAD_FILE);
261+
load_from_string_or_file_helper(INTERNAL_FUNCTION_PARAM_PASSTHRU, DOM_LOAD_FILE);
262+
}
263+
264+
PHP_METHOD(Dom_XMLDocument, createFromStream)
265+
{
266+
zval *stream_zv;
267+
dom_source_union source_union;
268+
const char *document_uri, *override_encoding = NULL;
269+
size_t document_uri_len, override_encoding_len;
270+
zend_long options = 0;
271+
if (zend_parse_parameters(
272+
ZEND_NUM_ARGS(),
273+
"r|p!lp!",
274+
&stream_zv,
275+
&document_uri,
276+
&document_uri_len,
277+
&options,
278+
&override_encoding,
279+
&override_encoding_len
280+
) == FAILURE) {
281+
RETURN_THROWS();
282+
}
283+
284+
php_stream_from_res(source_union.stream, Z_RES_P(stream_zv));
285+
286+
if (!check_options_validity(3, options)) {
287+
RETURN_THROWS();
288+
}
289+
290+
load_from_helper(return_value, DOM_LOAD_STREAM, source_union, options, override_encoding, document_uri);
254291
}
255292

256293
static int php_new_dom_write_smart_str(void *context, const char *buffer, int len)

0 commit comments

Comments
 (0)
Please sign in to comment.