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