diff --git a/NEWS b/NEWS
index e2499e27e05..37fe3f07666 100644
--- a/NEWS
+++ b/NEWS
@@ -324,6 +324,10 @@ PHP NEWS
- XMLReader:
. Declares class constant types. (Ayesh)
+ . Add XMLReader::fromStream(). (nielsdos)
+
+- XMLWriter:
+ . Add XMLWriter::toStream(). (nielsdos)
- XSL:
. Implement request #64137 (XSLTProcessor::setParameter() should allow both
diff --git a/UPGRADING b/UPGRADING
index 2df94b99144..9223dba06bd 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -178,6 +178,15 @@ PHP 8.4 UPGRADE NOTES
Passing an empty string to disable the handler is still allowed,
but not recommended.
+- XMLReader:
+ . Passing an invalid character encoding to XMLReader::open() or
+ XMLReader::XML() now throws a ValueError. Passing a string containing NULL
+ bytes previously emitted a warning and now throws a ValueError as well.
+
+- XMLWriter:
+ . Passing a string containing NULL bytes previously emitted a warning and
+ now throws a ValueError.
+
- XSL:
. XSLTProcessor::setParameter() will now throw a ValueError when its arguments
contain null bytes. This never actually worked correctly in the first place,
@@ -619,6 +628,14 @@ PHP 8.4 UPGRADE NOTES
array_any().
RFC: https://wiki.php.net/rfc/array_find
+- XMLReader:
+ . Added XMLReader::fromStream().
+ RFC: https://wiki.php.net/rfc/xmlreader_writer_streams
+
+- XMLWriter:
+ . Added XMLWriter::toStream().
+ RFC: https://wiki.php.net/rfc/xmlreader_writer_streams
+
- XSL:
. Added XSLTProcessor::registerPhpFunctionNS().
RFC: https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl
diff --git a/ext/xmlreader/php_xmlreader.c b/ext/xmlreader/php_xmlreader.c
index 0b3689ccf8f..50e98b8935a 100644
--- a/ext/xmlreader/php_xmlreader.c
+++ b/ext/xmlreader/php_xmlreader.c
@@ -167,8 +167,7 @@ static zend_function *xmlreader_get_method(zend_object **obj, zend_string *name,
/* There are only two static internal methods and they both have overrides. */
if (ZSTR_LEN(name) == sizeof("xml") - 1) {
return (zend_function *) &xmlreader_xml_fn;
- } else {
- ZEND_ASSERT(ZSTR_LEN(name) == sizeof("open") - 1);
+ } else if (ZSTR_LEN(name) == sizeof("open") - 1) {
return (zend_function *) &xmlreader_open_fn;
}
}
@@ -799,6 +798,22 @@ PHP_METHOD(XMLReader, next)
}
/* }}} */
+static bool xmlreader_valid_encoding(const char *encoding)
+{
+ if (!encoding) {
+ return true;
+ }
+
+ /* Normally we could use xmlTextReaderConstEncoding() afterwards but libxml2 < 2.12.0 has a bug of course
+ * where it returns NULL for some valid encodings instead. */
+ xmlCharEncodingHandlerPtr handler = xmlFindCharEncodingHandler(encoding);
+ if (!handler) {
+ return false;
+ }
+ xmlCharEncCloseFunc(handler);
+ return true;
+}
+
/* {{{ Sets the URI that the XMLReader will parse. */
PHP_METHOD(XMLReader, open)
{
@@ -811,7 +826,7 @@ PHP_METHOD(XMLReader, open)
char resolved_path[MAXPATHLEN + 1];
xmlTextReaderPtr reader = NULL;
- if (zend_parse_parameters(ZEND_NUM_ARGS(), "p|s!l", &source, &source_len, &encoding, &encoding_len, &options) == FAILURE) {
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "p|p!l", &source, &source_len, &encoding, &encoding_len, &options) == FAILURE) {
RETURN_THROWS();
}
@@ -827,9 +842,9 @@ PHP_METHOD(XMLReader, open)
RETURN_THROWS();
}
- if (encoding && CHECK_NULL_PATH(encoding, encoding_len)) {
- php_error_docref(NULL, E_WARNING, "Encoding must not contain NUL bytes");
- RETURN_FALSE;
+ if (!xmlreader_valid_encoding(encoding)) {
+ zend_argument_value_error(2, "must be a valid character encoding");
+ RETURN_THROWS();
}
valid_file = _xmlreader_get_valid_file_path(source, resolved_path, MAXPATHLEN );
@@ -859,6 +874,76 @@ PHP_METHOD(XMLReader, open)
}
/* }}} */
+static int xml_reader_stream_read(void *context, char *buffer, int len)
+{
+ zend_resource *resource = context;
+ if (EXPECTED(resource->ptr)) {
+ php_stream *stream = resource->ptr;
+ return php_stream_read(stream, buffer, len);
+ }
+ return -1;
+}
+
+static int xml_reader_stream_close(void *context)
+{
+ zend_resource *resource = context;
+ /* Don't close it as others may still use it! We don't own the resource!
+ * Just delete our reference (and clean up if we're the last one). */
+ zend_list_delete(resource);
+ return 0;
+}
+
+PHP_METHOD(XMLReader, fromStream)
+{
+ zval *stream_zv;
+ php_stream *stream;
+ char *document_uri = NULL;
+ char *encoding_name = NULL;
+ size_t document_uri_len, encoding_name_len;
+ zend_long flags = 0;
+
+ ZEND_PARSE_PARAMETERS_START(1, 4)
+ Z_PARAM_RESOURCE(stream_zv);
+ Z_PARAM_OPTIONAL
+ Z_PARAM_PATH_OR_NULL(encoding_name, encoding_name_len)
+ Z_PARAM_LONG(flags)
+ Z_PARAM_PATH_OR_NULL(document_uri, document_uri_len)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_stream_from_res(stream, Z_RES_P(stream_zv));
+
+ if (!xmlreader_valid_encoding(encoding_name)) {
+ zend_argument_value_error(2, "must be a valid character encoding");
+ RETURN_THROWS();
+ }
+
+ PHP_LIBXML_SANITIZE_GLOBALS(reader_for_stream);
+ xmlTextReaderPtr reader = xmlReaderForIO(
+ xml_reader_stream_read,
+ xml_reader_stream_close,
+ stream->res,
+ document_uri,
+ encoding_name,
+ flags
+ );
+ PHP_LIBXML_RESTORE_GLOBALS(reader_for_stream);
+
+ if (UNEXPECTED(reader == NULL)) {
+ zend_throw_error(NULL, "Could not construct libxml reader");
+ RETURN_THROWS();
+ }
+
+ /* When the reader is closed (even in error paths) the reference is destroyed. */
+ Z_ADDREF_P(stream_zv);
+
+ if (object_init_with_constructor(return_value, Z_CE_P(ZEND_THIS), 0, NULL, NULL) == SUCCESS) {
+ xmlreader_object *intern = Z_XMLREADER_P(return_value);
+ intern->ptr = reader;
+ } else {
+ xmlFreeTextReader(reader);
+ }
+}
+
/* Not Yet Implemented in libxml - functions exist just not coded
PHP_METHOD(XMLReader, resetState)
{
@@ -995,7 +1080,7 @@ PHP_METHOD(XMLReader, XML)
xmlParserInputBufferPtr inputbfr;
xmlTextReaderPtr reader;
- if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|s!l", &source, &source_len, &encoding, &encoding_len, &options) == FAILURE) {
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|p!l", &source, &source_len, &encoding, &encoding_len, &options) == FAILURE) {
RETURN_THROWS();
}
@@ -1011,9 +1096,9 @@ PHP_METHOD(XMLReader, XML)
RETURN_THROWS();
}
- if (encoding && CHECK_NULL_PATH(encoding, encoding_len)) {
- php_error_docref(NULL, E_WARNING, "Encoding must not contain NUL bytes");
- RETURN_FALSE;
+ if (!xmlreader_valid_encoding(encoding)) {
+ zend_argument_value_error(2, "must be a valid character encoding");
+ RETURN_THROWS();
}
inputbfr = xmlParserInputBufferCreateMem(source, source_len, XML_CHAR_ENCODING_NONE);
diff --git a/ext/xmlreader/php_xmlreader.stub.php b/ext/xmlreader/php_xmlreader.stub.php
index cbd705ff8aa..e10d7bc79c1 100644
--- a/ext/xmlreader/php_xmlreader.stub.php
+++ b/ext/xmlreader/php_xmlreader.stub.php
@@ -175,6 +175,9 @@ class XMLReader
/** @return bool|XMLReader */
public static function open(string $uri, ?string $encoding = null, int $flags = 0) {} // TODO Return type shouldn't be dependent on the call scope
+ /** @param resource $stream */
+ public static function fromStream($stream, ?string $encoding = null, int $flags = 0, ?string $documentUri = null): static {}
+
/** @tentative-return-type */
public function readInnerXml(): string {}
diff --git a/ext/xmlreader/php_xmlreader_arginfo.h b/ext/xmlreader/php_xmlreader_arginfo.h
index 2bf66258f02..7ff905f14d1 100644
--- a/ext/xmlreader/php_xmlreader_arginfo.h
+++ b/ext/xmlreader/php_xmlreader_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
- * Stub hash: fc01b7eacbd96dbeaf0b6a3a045692a77ef033f1 */
+ * Stub hash: 08ea43f5bbfa20407a6c1913fe3a51e99ba79fd8 */
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XMLReader_close, 0, 0, IS_TRUE, 0)
ZEND_END_ARG_INFO()
@@ -59,6 +59,13 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_XMLReader_open, 0, 0, 1)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0")
ZEND_END_ARG_INFO()
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_XMLReader_fromStream, 0, 1, IS_STATIC, 0)
+ ZEND_ARG_INFO(0, stream)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, encoding, IS_STRING, 1, "null")
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0")
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, documentUri, IS_STRING, 1, "null")
+ZEND_END_ARG_INFO()
+
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XMLReader_readInnerXml, 0, 0, IS_STRING, 0)
ZEND_END_ARG_INFO()
@@ -107,6 +114,7 @@ ZEND_METHOD(XMLReader, moveToNextAttribute);
ZEND_METHOD(XMLReader, read);
ZEND_METHOD(XMLReader, next);
ZEND_METHOD(XMLReader, open);
+ZEND_METHOD(XMLReader, fromStream);
ZEND_METHOD(XMLReader, readInnerXml);
ZEND_METHOD(XMLReader, readOuterXml);
ZEND_METHOD(XMLReader, readString);
@@ -134,6 +142,7 @@ static const zend_function_entry class_XMLReader_methods[] = {
ZEND_ME(XMLReader, read, arginfo_class_XMLReader_read, ZEND_ACC_PUBLIC)
ZEND_ME(XMLReader, next, arginfo_class_XMLReader_next, ZEND_ACC_PUBLIC)
ZEND_ME(XMLReader, open, arginfo_class_XMLReader_open, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+ ZEND_ME(XMLReader, fromStream, arginfo_class_XMLReader_fromStream, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
ZEND_ME(XMLReader, readInnerXml, arginfo_class_XMLReader_readInnerXml, ZEND_ACC_PUBLIC)
ZEND_ME(XMLReader, readOuterXml, arginfo_class_XMLReader_readOuterXml, ZEND_ACC_PUBLIC)
ZEND_ME(XMLReader, readString, arginfo_class_XMLReader_readString, ZEND_ACC_PUBLIC)
diff --git a/ext/xmlreader/tests/bug73246.phpt b/ext/xmlreader/tests/bug73246.phpt
index 96f9e65d997..f20c1604538 100644
--- a/ext/xmlreader/tests/bug73246.phpt
+++ b/ext/xmlreader/tests/bug73246.phpt
@@ -1,14 +1,27 @@
---TEST--
-Bug #73246 (XMLReader: encoding length not checked)
+--TEST--
+Bug #73246 (XMLReader: encoding length not checked)
--EXTENSIONS--
xmlreader
---FILE--
-open(__FILE__, "UTF\0-8");
-$reader->XML('', "UTF\0-8");
-?>
---EXPECTF--
-Warning: XMLReader::open(): Encoding must not contain NUL bytes in %s on line %d
-
-Warning: XMLReader::XML(): Encoding must not contain NUL bytes in %s on line %d
+--FILE--
+open(__FILE__, "UTF\0-8");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+try {
+ $reader->XML('', "UTF\0-8");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+try {
+ XMLReader::fromStream(fopen('php://memory', 'r'), encoding: "UTF\0-8");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+XMLReader::open(): Argument #2 ($encoding) must not contain any null bytes
+XMLReader::XML(): Argument #2 ($encoding) must not contain any null bytes
+XMLReader::fromStream(): Argument #2 ($encoding) must not contain any null bytes
diff --git a/ext/xmlreader/tests/fromStream_broken_stream.phpt b/ext/xmlreader/tests/fromStream_broken_stream.phpt
new file mode 100644
index 00000000000..2dfa246ebf7
--- /dev/null
+++ b/ext/xmlreader/tests/fromStream_broken_stream.phpt
@@ -0,0 +1,37 @@
+--TEST--
+XMLReader::fromStream() - broken stream
+--EXTENSIONS--
+xmlreader
+--FILE--
+");
+fseek($h, 0);
+
+$reader = XMLReader::fromStream($h, encoding: "UTF-8");
+$start = true;
+while ($result = @$reader->read()) {
+ var_dump($result);
+ switch ($reader->nodeType) {
+ case XMLReader::ELEMENT:
+ echo "Element: ", $reader->name, "\n";
+ break;
+ case XMLReader::COMMENT:
+ echo "Comment: ", $reader->value, "\n";
+ break;
+ }
+
+ if ($start) {
+ fwrite($h, "");
+ fclose($h);
+ $start = false;
+ }
+}
+var_dump($reader->depth);
+?>
+--EXPECT--
+bool(true)
+Element: root
+bool(true)
+Comment: my comment
+int(1)
diff --git a/ext/xmlreader/tests/fromStream_custom_constructor.phpt b/ext/xmlreader/tests/fromStream_custom_constructor.phpt
new file mode 100644
index 00000000000..4c94c4a4bbf
--- /dev/null
+++ b/ext/xmlreader/tests/fromStream_custom_constructor.phpt
@@ -0,0 +1,62 @@
+--TEST--
+XMLReader::fromStream() - custom constructor
+--EXTENSIONS--
+xmlreader
+--FILE--
+myField = 1234;
+ echo "hello world\n";
+ }
+}
+
+$h = fopen("php://memory", "w+");
+fwrite($h, "");
+fseek($h, 0);
+
+$reader = CustomXMLReader::fromStream($h, encoding: "UTF-8");
+var_dump($reader);
+var_dump($reader->read());
+var_dump($reader->nodeType);
+
+fclose($h);
+?>
+--EXPECTF--
+hello world
+object(CustomXMLReader)#%d (1) {
+ ["attributeCount"]=>
+ uninitialized(int)
+ ["baseURI"]=>
+ uninitialized(string)
+ ["depth"]=>
+ uninitialized(int)
+ ["hasAttributes"]=>
+ uninitialized(bool)
+ ["hasValue"]=>
+ uninitialized(bool)
+ ["isDefault"]=>
+ uninitialized(bool)
+ ["isEmptyElement"]=>
+ uninitialized(bool)
+ ["localName"]=>
+ uninitialized(string)
+ ["name"]=>
+ uninitialized(string)
+ ["namespaceURI"]=>
+ uninitialized(string)
+ ["nodeType"]=>
+ uninitialized(int)
+ ["prefix"]=>
+ uninitialized(string)
+ ["value"]=>
+ uninitialized(string)
+ ["xmlLang"]=>
+ uninitialized(string)
+ ["myField"]=>
+ int(1234)
+}
+bool(true)
+int(1)
diff --git a/ext/xmlreader/tests/fromStream_custom_constructor_error.phpt b/ext/xmlreader/tests/fromStream_custom_constructor_error.phpt
new file mode 100644
index 00000000000..60e65fa9211
--- /dev/null
+++ b/ext/xmlreader/tests/fromStream_custom_constructor_error.phpt
@@ -0,0 +1,26 @@
+--TEST--
+XMLReader::fromStream() - custom constructor with error
+--EXTENSIONS--
+xmlreader
+--FILE--
+");
+fseek($h, 0);
+
+try {
+ CustomXMLReader::fromStream($h, encoding: "UTF-8");
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+fclose($h);
+?>
+--EXPECT--
+nope
diff --git a/ext/xmlreader/tests/fromStream_legit_usage.phpt b/ext/xmlreader/tests/fromStream_legit_usage.phpt
new file mode 100644
index 00000000000..3940d9659f5
--- /dev/null
+++ b/ext/xmlreader/tests/fromStream_legit_usage.phpt
@@ -0,0 +1,34 @@
+--TEST--
+XMLReader::fromStream() - legit usage
+--EXTENSIONS--
+xmlreader
+--FILE--
+");
+fseek($h, 0);
+
+$reader = XMLReader::fromStream($h, encoding: "UTF-8");
+while ($reader->read()) {
+ switch ($reader->nodeType) {
+ case XMLReader::ELEMENT:
+ echo "Element: ", $reader->name, "\n";
+ break;
+ case XMLReader::COMMENT:
+ echo "Comment: ", $reader->value, "\n";
+ break;
+ }
+}
+
+// Force cleanup of stream reference
+unset($reader);
+
+var_dump(ftell($h));
+
+fclose($h);
+?>
+--EXPECT--
+Element: root
+Comment: my comment
+Element: child
+int(38)
diff --git a/ext/xmlreader/tests/invalid_encoding.phpt b/ext/xmlreader/tests/invalid_encoding.phpt
new file mode 100644
index 00000000000..c3918855199
--- /dev/null
+++ b/ext/xmlreader/tests/invalid_encoding.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Passing an invalid character encoding
+--EXTENSIONS--
+xmlreader
+--FILE--
+open(__FILE__, "does not exist");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+$h = fopen("php://memory", "w+");
+try {
+ XMLReader::fromStream($h, encoding: "does not exist");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+fclose($h);
+
+try {
+ $reader->XML('', "does not exist");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+XMLReader::open(): Argument #2 ($encoding) must be a valid character encoding
+XMLReader::fromStream(): Argument #2 ($encoding) must be a valid character encoding
+XMLReader::XML(): Argument #2 ($encoding) must be a valid character encoding
diff --git a/ext/xmlreader/tests/libxml_global_state_entity_loader_bypass.phpt b/ext/xmlreader/tests/libxml_global_state_entity_loader_bypass.phpt
index a0223367c70..a2b5adbb0f8 100644
--- a/ext/xmlreader/tests/libxml_global_state_entity_loader_bypass.phpt
+++ b/ext/xmlreader/tests/libxml_global_state_entity_loader_bypass.phpt
@@ -19,11 +19,18 @@ zend_test_override_libxml_global_state();
echo "--- String test ---\n";
$reader = XMLReader::xml($xml);
$reader->read();
+
echo "--- File test ---\n";
file_put_contents("libxml_global_state_entity_loader_bypass.tmp", $xml);
$reader = XMLReader::open("libxml_global_state_entity_loader_bypass.tmp");
$reader->read();
+echo "--- Stream test ---\n";
+$stream = fopen("libxml_global_state_entity_loader_bypass.tmp", "r");
+$reader = XMLReader::fromStream($stream);
+$reader->read();
+fclose($stream);
+
echo "Done\n";
?>
@@ -34,4 +41,5 @@ echo "Done\n";
--EXPECT--
--- String test ---
--- File test ---
+--- Stream test ---
Done
diff --git a/ext/xmlwriter/php_xmlwriter.c b/ext/xmlwriter/php_xmlwriter.c
index 59a93292629..45ba59f32de 100644
--- a/ext/xmlwriter/php_xmlwriter.c
+++ b/ext/xmlwriter/php_xmlwriter.c
@@ -45,12 +45,8 @@ typedef int (*xmlwriter_read_int_t)(xmlTextWriterPtr writer);
static zend_object_handlers xmlwriter_object_handlers;
-/* {{{{ xmlwriter_object_dtor */
-static void xmlwriter_object_dtor(zend_object *object)
+static zend_always_inline void xmlwriter_destroy_libxml_objects(ze_xmlwriter_object *intern)
{
- ze_xmlwriter_object *intern = php_xmlwriter_fetch_object(object);
-
- /* freeing the resource here may leak, but otherwise we may use it after it has been freed */
if (intern->ptr) {
xmlFreeTextWriter(intern->ptr);
intern->ptr = NULL;
@@ -59,6 +55,15 @@ static void xmlwriter_object_dtor(zend_object *object)
xmlBufferFree(intern->output);
intern->output = NULL;
}
+}
+
+/* {{{{ xmlwriter_object_dtor */
+static void xmlwriter_object_dtor(zend_object *object)
+{
+ ze_xmlwriter_object *intern = php_xmlwriter_fetch_object(object);
+
+ /* freeing the resource here may leak, but otherwise we may use it after it has been freed */
+ xmlwriter_destroy_libxml_objects(intern);
zend_objects_destroy_object(object);
}
/* }}} */
@@ -583,7 +588,7 @@ PHP_FUNCTION(xmlwriter_start_document)
int retval;
zval *self;
- if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "O|s!s!s!", &self, xmlwriter_class_entry_ce, &version, &version_len, &enc, &enc_len, &alone, &alone_len) == FAILURE) {
+ if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "O|s!p!s!", &self, xmlwriter_class_entry_ce, &version, &version_len, &enc, &enc_len, &alone, &alone_len) == FAILURE) {
RETURN_THROWS();
}
XMLWRITER_FROM_OBJECT(ptr, self);
@@ -818,12 +823,7 @@ PHP_FUNCTION(xmlwriter_open_uri)
}
if (self) {
- if (ze_obj->ptr) {
- xmlFreeTextWriter(ze_obj->ptr);
- }
- if (ze_obj->output) {
- xmlBufferFree(ze_obj->output);
- }
+ xmlwriter_destroy_libxml_objects(ze_obj);
ze_obj->ptr = ptr;
ze_obj->output = NULL;
RETURN_TRUE;
@@ -867,12 +867,7 @@ PHP_FUNCTION(xmlwriter_open_memory)
}
if (self) {
- if (ze_obj->ptr) {
- xmlFreeTextWriter(ze_obj->ptr);
- }
- if (ze_obj->output) {
- xmlBufferFree(ze_obj->output);
- }
+ xmlwriter_destroy_libxml_objects(ze_obj);
ze_obj->ptr = ptr;
ze_obj->output = buffer;
RETURN_TRUE;
@@ -886,6 +881,62 @@ PHP_FUNCTION(xmlwriter_open_memory)
}
/* }}} */
+static int xml_writer_stream_write(void *context, const char *buffer, int len)
+{
+ zend_resource *resource = context;
+ if (EXPECTED(resource->ptr)) {
+ php_stream *stream = resource->ptr;
+ return php_stream_write(stream, buffer, len);
+ }
+ return -1;
+}
+
+static int xml_writer_stream_close(void *context)
+{
+ zend_resource *resource = context;
+ /* Don't close it as others may still use it! We don't own the resource!
+ * Just delete our reference (and clean up if we're the last one). */
+ zend_list_delete(resource);
+ return 0;
+}
+
+PHP_METHOD(XMLWriter, toStream)
+{
+ zval *stream_zv;
+ php_stream *stream;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_RESOURCE(stream_zv)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_stream_from_res(stream, Z_RES_P(stream_zv));
+
+ xmlOutputBufferPtr output_buffer = xmlOutputBufferCreateIO(xml_writer_stream_write, xml_writer_stream_close, stream->res, NULL);
+ if (UNEXPECTED(output_buffer == NULL)) {
+ zend_throw_error(NULL, "Could not construct libxml output buffer");
+ RETURN_THROWS();
+ }
+
+ /* When the buffer is closed (even in error paths) the reference is destroyed. */
+ Z_ADDREF_P(stream_zv);
+
+ xmlTextWriterPtr writer = xmlNewTextWriter(output_buffer);
+ if (UNEXPECTED(writer == NULL)) {
+ xmlOutputBufferClose(output_buffer);
+ zend_throw_error(NULL, "Could not construct libxml writer");
+ RETURN_THROWS();
+ }
+
+ if (object_init_with_constructor(return_value, Z_CE_P(ZEND_THIS), 0, NULL, NULL) == SUCCESS) {
+ ze_xmlwriter_object *intern = Z_XMLWRITER_P(return_value);
+ intern->ptr = writer;
+ /* output_buffer is owned by writer, and so writer will clean that up for us. */
+ intern->output = NULL;
+ } else {
+ xmlFreeTextWriter(writer);
+ }
+}
+
/* {{{ php_xmlwriter_flush */
static void php_xmlwriter_flush(INTERNAL_FUNCTION_PARAMETERS, int force_string) {
xmlTextWriterPtr ptr;
diff --git a/ext/xmlwriter/php_xmlwriter.stub.php b/ext/xmlwriter/php_xmlwriter.stub.php
index 7f3ff5be103..753478d586a 100644
--- a/ext/xmlwriter/php_xmlwriter.stub.php
+++ b/ext/xmlwriter/php_xmlwriter.stub.php
@@ -102,6 +102,9 @@ class XMLWriter
*/
public function openMemory(): bool {}
+ /** @param resource $stream */
+ public static function toStream($stream): static {}
+
/**
* @tentative-return-type
* @alias xmlwriter_set_indent
diff --git a/ext/xmlwriter/php_xmlwriter_arginfo.h b/ext/xmlwriter/php_xmlwriter_arginfo.h
index cfa558bd3dc..bafd8fbdd04 100644
--- a/ext/xmlwriter/php_xmlwriter_arginfo.h
+++ b/ext/xmlwriter/php_xmlwriter_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
- * Stub hash: 820ad2d68166b189b9163c2c3dfcc76806d41b7d */
+ * Stub hash: c65d664c3c84742dfda4cb3e2682036ec4fe893a */
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_xmlwriter_open_uri, 0, 1, XMLWriter, MAY_BE_FALSE)
ZEND_ARG_TYPE_INFO(0, uri, IS_STRING, 0)
@@ -182,6 +182,10 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XMLWriter_openMemory, 0, 0, _IS_BOOL, 0)
ZEND_END_ARG_INFO()
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_XMLWriter_toStream, 0, 1, IS_STATIC, 0)
+ ZEND_ARG_INFO(0, stream)
+ZEND_END_ARG_INFO()
+
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XMLWriter_setIndent, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, enable, _IS_BOOL, 0)
ZEND_END_ARG_INFO()
@@ -369,6 +373,7 @@ ZEND_FUNCTION(xmlwriter_end_dtd_entity);
ZEND_FUNCTION(xmlwriter_write_dtd_entity);
ZEND_FUNCTION(xmlwriter_output_memory);
ZEND_FUNCTION(xmlwriter_flush);
+ZEND_METHOD(XMLWriter, toStream);
static const zend_function_entry ext_functions[] = {
ZEND_FE(xmlwriter_open_uri, arginfo_xmlwriter_open_uri)
@@ -419,6 +424,7 @@ static const zend_function_entry ext_functions[] = {
static const zend_function_entry class_XMLWriter_methods[] = {
ZEND_RAW_FENTRY("openUri", zif_xmlwriter_open_uri, arginfo_class_XMLWriter_openUri, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("openMemory", zif_xmlwriter_open_memory, arginfo_class_XMLWriter_openMemory, ZEND_ACC_PUBLIC, NULL, NULL)
+ ZEND_ME(XMLWriter, toStream, arginfo_class_XMLWriter_toStream, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
ZEND_RAW_FENTRY("setIndent", zif_xmlwriter_set_indent, arginfo_class_XMLWriter_setIndent, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("setIndentString", zif_xmlwriter_set_indent_string, arginfo_class_XMLWriter_setIndentString, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("startComment", zif_xmlwriter_start_comment, arginfo_class_XMLWriter_startComment, ZEND_ACC_PUBLIC, NULL, NULL)
diff --git a/ext/xmlwriter/tests/xmlwriter_toStream_custom_constructor.phpt b/ext/xmlwriter/tests/xmlwriter_toStream_custom_constructor.phpt
new file mode 100644
index 00000000000..dfa766c49d4
--- /dev/null
+++ b/ext/xmlwriter/tests/xmlwriter_toStream_custom_constructor.phpt
@@ -0,0 +1,32 @@
+--TEST--
+XMLWriter::toStream() - custom constructor
+--EXTENSIONS--
+xmlwriter
+--FILE--
+myField = 1234;
+ echo "hello world\n";
+ }
+}
+
+$h = fopen("php://output", "w");
+
+$writer = CustomXMLWriter::toStream($h);
+var_dump($writer);
+$writer->startElement("root");
+$writer->endElement();
+$writer->flush();
+
+?>
+--EXPECTF--
+hello world
+object(CustomXMLWriter)#%d (1) {
+ ["myField"]=>
+ int(1234)
+}
+
diff --git a/ext/xmlwriter/tests/xmlwriter_toStream_custom_constructor_error.phpt b/ext/xmlwriter/tests/xmlwriter_toStream_custom_constructor_error.phpt
new file mode 100644
index 00000000000..636d8ffbbc0
--- /dev/null
+++ b/ext/xmlwriter/tests/xmlwriter_toStream_custom_constructor_error.phpt
@@ -0,0 +1,24 @@
+--TEST--
+XMLWriter::toStream() - custom constructor error
+--EXTENSIONS--
+xmlwriter
+--FILE--
+getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+nope
diff --git a/ext/xmlwriter/tests/xmlwriter_toStream_encoding_gbk.phpt b/ext/xmlwriter/tests/xmlwriter_toStream_encoding_gbk.phpt
new file mode 100644
index 00000000000..a92f3f6c582
--- /dev/null
+++ b/ext/xmlwriter/tests/xmlwriter_toStream_encoding_gbk.phpt
@@ -0,0 +1,18 @@
+--TEST--
+XMLWriter::toStream() with encoding - test GBK
+--EXTENSIONS--
+xmlwriter
+--FILE--
+startDocument(encoding: "GBK");
+$writer->writeComment("\u{00E9}\u{00E9}\u{00E9}");
+unset($writer);
+
+?>
+--EXPECT--
+
+
diff --git a/ext/xmlwriter/tests/xmlwriter_toStream_encoding_utf8.phpt b/ext/xmlwriter/tests/xmlwriter_toStream_encoding_utf8.phpt
new file mode 100644
index 00000000000..823f6337d50
--- /dev/null
+++ b/ext/xmlwriter/tests/xmlwriter_toStream_encoding_utf8.phpt
@@ -0,0 +1,18 @@
+--TEST--
+XMLWriter::toStream() with encoding - test UTF-8
+--EXTENSIONS--
+xmlwriter
+--FILE--
+startDocument(encoding: "UTF-8");
+$writer->writeComment('ééé');
+unset($writer);
+
+?>
+--EXPECT--
+
+
diff --git a/ext/xmlwriter/tests/xmlwriter_toStream_invalidate_stream.phpt b/ext/xmlwriter/tests/xmlwriter_toStream_invalidate_stream.phpt
new file mode 100644
index 00000000000..d0bbf73532c
--- /dev/null
+++ b/ext/xmlwriter/tests/xmlwriter_toStream_invalidate_stream.phpt
@@ -0,0 +1,20 @@
+--TEST--
+XMLWriter::toStream() - invalidating stream
+--EXTENSIONS--
+xmlwriter
+--FILE--
+startElement("root");
+fclose($h);
+$writer->writeAttribute("align", "left");
+$writer->endElement();
+var_dump($writer->flush());
+unset($writer);
+
+?>
+--EXPECT--
+int(-1)
diff --git a/ext/xmlwriter/tests/xmlwriter_toStream_normal_usage.phpt b/ext/xmlwriter/tests/xmlwriter_toStream_normal_usage.phpt
new file mode 100644
index 00000000000..d9db89908b3
--- /dev/null
+++ b/ext/xmlwriter/tests/xmlwriter_toStream_normal_usage.phpt
@@ -0,0 +1,30 @@
+--TEST--
+XMLWriter::toStream() - normal usage
+--EXTENSIONS--
+xmlwriter
+--FILE--
+startElement("root");
+$writer->writeAttribute("align", "left");
+$writer->writeComment("hello");
+$writer->endElement();
+$amount = $writer->flush();
+echo "\nFlush amount: ";
+var_dump($amount);
+
+// Force destroying the held stream
+unset($writer);
+
+// Test that the stream wasn't closed or destroyed
+fwrite($h, "\nthis is the end\n");
+
+?>
+--EXPECT--
+
+Flush amount: int(38)
+
+this is the end
diff --git a/ext/xmlwriter/tests/xmlwriter_toStream_open_invalidated_stream.phpt b/ext/xmlwriter/tests/xmlwriter_toStream_open_invalidated_stream.phpt
new file mode 100644
index 00000000000..b6c6733bb7f
--- /dev/null
+++ b/ext/xmlwriter/tests/xmlwriter_toStream_open_invalidated_stream.phpt
@@ -0,0 +1,19 @@
+--TEST--
+XMLWriter::toStream() - open invalidated stream
+--EXTENSIONS--
+xmlwriter
+--FILE--
+getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+XMLWriter::toStream(): supplied resource is not a valid stream resource