diff --git a/NEWS b/NEWS
index 28b0a66861c..9cd93cfce7a 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,12 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.3.8
+- DOM:
+ . Fix crashes when entity declaration is removed while still having entity
+ references. (nielsdos)
+ . Fix references not handled correctly in C14N. (nielsdos)
+ . Fix crash when calling childNodes next() when iterator is exhausted.
+ (nielsdos)
09 May 2024, PHP 8.3.7
diff --git a/ext/dom/dom_iterators.c b/ext/dom/dom_iterators.c
index 21d59c5f02b..3f80ffc817c 100644
--- a/ext/dom/dom_iterators.c
+++ b/ext/dom/dom_iterators.c
@@ -148,8 +148,7 @@ static int php_dom_iterator_valid(zend_object_iterator *iter) /* {{{ */
zval *php_dom_iterator_current_data(zend_object_iterator *iter) /* {{{ */
{
php_dom_iterator *iterator = (php_dom_iterator *)iter;
-
- return &iterator->curobj;
+ return Z_ISUNDEF(iterator->curobj) ? NULL : &iterator->curobj;
}
/* }}} */
diff --git a/ext/dom/dom_properties.h b/ext/dom/dom_properties.h
index 5a371304a1f..4e5e5e2db70 100644
--- a/ext/dom/dom_properties.h
+++ b/ext/dom/dom_properties.h
@@ -85,6 +85,11 @@ int dom_entity_actual_encoding_read(dom_object *obj, zval *retval);
int dom_entity_encoding_read(dom_object *obj, zval *retval);
int dom_entity_version_read(dom_object *obj, zval *retval);
+/* entity reference properties */
+int dom_entity_reference_child_read(dom_object *obj, zval *retval);
+int dom_entity_reference_text_content_read(dom_object *obj, zval *retval);
+int dom_entity_reference_child_nodes_read(dom_object *obj, zval *retval);
+
/* namednodemap properties */
int dom_namednodemap_length_read(dom_object *obj, zval *retval);
diff --git a/ext/dom/entityreference.c b/ext/dom/entityreference.c
index 61f0b92eedc..30928b1c6d1 100644
--- a/ext/dom/entityreference.c
+++ b/ext/dom/entityreference.c
@@ -22,6 +22,7 @@
#include "php.h"
#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
#include "php_dom.h"
+#include "dom_properties.h"
/*
* class DOMEntityReference extends DOMNode
@@ -65,4 +66,63 @@ PHP_METHOD(DOMEntityReference, __construct)
}
/* }}} end DOMEntityReference::__construct */
+/* The following property handlers are necessary because of special lifetime management with entities and entity
+ * references. The issue is that entity references hold a reference to an entity declaration, but don't
+ * register that reference anywhere. When the entity declaration disappears we have no way of notifying the
+ * entity references. Override the property handlers for the declaration-accessing properties to fix this problem. */
+
+xmlEntityPtr dom_entity_reference_fetch_and_sync_declaration(xmlNodePtr reference)
+{
+ xmlEntityPtr entity = xmlGetDocEntity(reference->doc, reference->name);
+ reference->children = (xmlNodePtr) entity;
+ reference->last = (xmlNodePtr) entity;
+ reference->content = entity ? entity->content : NULL;
+ return entity;
+}
+
+int dom_entity_reference_child_read(dom_object *obj, zval *retval)
+{
+ xmlNodePtr nodep = dom_object_get_node(obj);
+
+ if (nodep == NULL) {
+ php_dom_throw_error(INVALID_STATE_ERR, true);
+ return FAILURE;
+ }
+
+ xmlEntityPtr entity = dom_entity_reference_fetch_and_sync_declaration(nodep);
+ if (entity == NULL) {
+ ZVAL_NULL(retval);
+ return SUCCESS;
+ }
+
+ php_dom_create_object((xmlNodePtr) entity, retval, obj);
+ return SUCCESS;
+}
+
+int dom_entity_reference_text_content_read(dom_object *obj, zval *retval)
+{
+ xmlNodePtr nodep = dom_object_get_node(obj);
+
+ if (nodep == NULL) {
+ php_dom_throw_error(INVALID_STATE_ERR, true);
+ return FAILURE;
+ }
+
+ dom_entity_reference_fetch_and_sync_declaration(nodep);
+ return dom_node_text_content_read(obj, retval);
+}
+
+int dom_entity_reference_child_nodes_read(dom_object *obj, zval *retval)
+{
+ xmlNodePtr nodep = dom_object_get_node(obj);
+
+ if (nodep == NULL) {
+ php_dom_throw_error(INVALID_STATE_ERR, true);
+ return FAILURE;
+ }
+
+ dom_entity_reference_fetch_and_sync_declaration(nodep);
+ return dom_node_child_nodes_read(obj, retval);
+}
+
#endif
diff --git a/ext/dom/node.c b/ext/dom/node.c
index 43f03f5e96b..7cc3cbff1ef 100644
--- a/ext/dom/node.c
+++ b/ext/dom/node.c
@@ -1821,7 +1821,7 @@ static void dom_canonicalization(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{
char *xquery;
/* Find "query" key */
- tmp = zend_hash_find(ht, ZSTR_KNOWN(ZEND_STR_QUERY));
+ tmp = zend_hash_find_deref(ht, ZSTR_KNOWN(ZEND_STR_QUERY));
if (!tmp) {
/* if mode == 0 then $xpath arg is 3, if mode == 1 then $xpath is 4 */
zend_argument_value_error(3 + mode, "must have a \"query\" key");
@@ -1837,12 +1837,13 @@ static void dom_canonicalization(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{
ctxp = xmlXPathNewContext(docp);
ctxp->node = nodep;
- tmp = zend_hash_str_find(ht, "namespaces", sizeof("namespaces")-1);
+ tmp = zend_hash_str_find_deref(ht, "namespaces", sizeof("namespaces")-1);
if (tmp && Z_TYPE_P(tmp) == IS_ARRAY && !HT_IS_PACKED(Z_ARRVAL_P(tmp))) {
zval *tmpns;
zend_string *prefix;
ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(tmp), prefix, tmpns) {
+ ZVAL_DEREF(tmpns);
if (Z_TYPE_P(tmpns) == IS_STRING) {
if (prefix) {
xmlXPathRegisterNs(ctxp, (xmlChar *) ZSTR_VAL(prefix), (xmlChar *) Z_STRVAL_P(tmpns));
diff --git a/ext/dom/nodelist.c b/ext/dom/nodelist.c
index c94a6fd5105..96868b05437 100644
--- a/ext/dom/nodelist.c
+++ b/ext/dom/nodelist.c
@@ -49,6 +49,16 @@ static zend_always_inline void reset_objmap_cache(dom_nnodemap_object *objmap)
objmap->cached_length = -1;
}
+static xmlNodePtr dom_nodelist_iter_start_first_child(xmlNodePtr nodep)
+{
+ if (nodep->type == XML_ENTITY_REF_NODE) {
+ /* See entityreference.c */
+ dom_entity_reference_fetch_and_sync_declaration(nodep);
+ }
+
+ return nodep->children;
+}
+
int php_dom_get_nodelist_length(dom_object *obj)
{
dom_nnodemap_object *objmap = (dom_nnodemap_object *) obj->ptr;
@@ -83,7 +93,7 @@ int php_dom_get_nodelist_length(dom_object *obj)
int count = 0;
if (objmap->nodetype == XML_ATTRIBUTE_NODE || objmap->nodetype == XML_ELEMENT_NODE) {
- xmlNodePtr curnode = nodep->children;
+ xmlNodePtr curnode = dom_nodelist_iter_start_first_child(nodep);
if (curnode) {
count++;
while (curnode->next != NULL) {
@@ -185,7 +195,7 @@ void php_dom_nodelist_get_item_into_zval(dom_nnodemap_object *objmap, zend_long
int count = 0;
if (objmap->nodetype == XML_ATTRIBUTE_NODE || objmap->nodetype == XML_ELEMENT_NODE) {
if (restart) {
- nodep = nodep->children;
+ nodep = dom_nodelist_iter_start_first_child(nodep);
}
while (count < relative_index && nodep != NULL) {
count++;
diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c
index 5ce98a4aeb3..ff598f1ee54 100644
--- a/ext/dom/php_dom.c
+++ b/ext/dom/php_dom.c
@@ -72,6 +72,7 @@ static HashTable classes;
static HashTable dom_document_prop_handlers;
static HashTable dom_documentfragment_prop_handlers;
static HashTable dom_node_prop_handlers;
+static HashTable dom_entity_reference_prop_handlers;
static HashTable dom_nodelist_prop_handlers;
static HashTable dom_namednodemap_prop_handlers;
static HashTable dom_characterdata_prop_handlers;
@@ -286,6 +287,14 @@ static void dom_register_prop_handler(HashTable *prop_handler, char *name, size_
zend_string_release_ex(str, 1);
}
+static void dom_override_prop_handler(HashTable *prop_handler, char *name, size_t name_len, dom_read_t read_func, dom_write_t write_func)
+{
+ dom_prop_handler hnd;
+ hnd.read_func = read_func;
+ hnd.write_func = write_func;
+ zend_hash_str_update_mem(prop_handler, name, name_len, &hnd, sizeof(dom_prop_handler));
+}
+
static zval *dom_get_property_ptr_ptr(zend_object *object, zend_string *name, int type, void **cache_slot)
{
dom_object *obj = php_dom_obj_from_obj(object);
@@ -815,7 +824,14 @@ PHP_MINIT_FUNCTION(dom)
dom_entityreference_class_entry = register_class_DOMEntityReference(dom_node_class_entry);
dom_entityreference_class_entry->create_object = dom_objects_new;
- zend_hash_add_ptr(&classes, dom_entityreference_class_entry->name, &dom_node_prop_handlers);
+
+ zend_hash_init(&dom_entity_reference_prop_handlers, 0, NULL, dom_dtor_prop_handler, true);
+ zend_hash_merge(&dom_entity_reference_prop_handlers, &dom_node_prop_handlers, dom_copy_prop_handler, false);
+ dom_override_prop_handler(&dom_entity_reference_prop_handlers, "firstChild", sizeof("firstChild")-1, dom_entity_reference_child_read, NULL);
+ dom_override_prop_handler(&dom_entity_reference_prop_handlers, "lastChild", sizeof("lastChild")-1, dom_entity_reference_child_read, NULL);
+ dom_override_prop_handler(&dom_entity_reference_prop_handlers, "textContent", sizeof("textContent")-1, dom_entity_reference_text_content_read, NULL);
+ dom_override_prop_handler(&dom_entity_reference_prop_handlers, "childNodes", sizeof("childNodes")-1, dom_entity_reference_child_nodes_read, NULL);
+ zend_hash_add_ptr(&classes, dom_entityreference_class_entry->name, &dom_entity_reference_prop_handlers);
dom_processinginstruction_class_entry = register_class_DOMProcessingInstruction(dom_node_class_entry);
dom_processinginstruction_class_entry->create_object = dom_objects_new;
@@ -878,6 +894,7 @@ PHP_MSHUTDOWN_FUNCTION(dom) /* {{{ */
zend_hash_destroy(&dom_document_prop_handlers);
zend_hash_destroy(&dom_documentfragment_prop_handlers);
zend_hash_destroy(&dom_node_prop_handlers);
+ zend_hash_destroy(&dom_entity_reference_prop_handlers);
zend_hash_destroy(&dom_namespace_node_prop_handlers);
zend_hash_destroy(&dom_nodelist_prop_handlers);
zend_hash_destroy(&dom_namednodemap_prop_handlers);
diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h
index c8d13c220de..cebbe47f265 100644
--- a/ext/dom/php_dom.h
+++ b/ext/dom/php_dom.h
@@ -153,6 +153,7 @@ zend_string *dom_node_get_node_name_attribute_or_element(const xmlNode *nodep);
bool php_dom_is_node_connected(const xmlNode *node);
bool php_dom_adopt_node(xmlNodePtr nodep, dom_object *dom_object_new_document, xmlDocPtr new_document);
xmlNsPtr dom_get_ns_resolve_prefix_conflict(xmlNodePtr tree, const char *uri);
+xmlEntityPtr dom_entity_reference_fetch_and_sync_declaration(xmlNodePtr reference);
/* parentnode */
void dom_parent_node_prepend(dom_object *context, zval *nodes, uint32_t nodesc);
diff --git a/ext/dom/tests/DOMNode_C14N_references.phpt b/ext/dom/tests/DOMNode_C14N_references.phpt
new file mode 100644
index 00000000000..514e22be636
--- /dev/null
+++ b/ext/dom/tests/DOMNode_C14N_references.phpt
@@ -0,0 +1,41 @@
+--TEST--
+Test: Canonicalization - C14N() with references
+--EXTENSIONS--
+dom
+--FILE--
+
+
+
+
+
+
+
+
+EOXML;
+
+$dom = new DOMDocument();
+$dom->loadXML($xml);
+$doc = $dom->documentElement->firstChild;
+
+$xpath = [
+ 'query' => '(//a:contain | //a:bar | .//namespace::*)',
+ 'namespaces' => ['a' => 'http://www.example.com/ns/foo'],
+];
+$prefixes = ['test'];
+
+foreach ($xpath['namespaces'] as $k => &$v);
+unset($v);
+foreach ($xpath as $k => &$v);
+unset($v);
+foreach ($prefixes as $k => &$v);
+unset($v);
+
+echo $doc->C14N(true, false, $xpath, $prefixes);
+?>
+--EXPECT--
+
diff --git a/ext/dom/tests/childNodes_current_crash.phpt b/ext/dom/tests/childNodes_current_crash.phpt
new file mode 100644
index 00000000000..aa93cf33a64
--- /dev/null
+++ b/ext/dom/tests/childNodes_current_crash.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Crash in childNodes iterator current()
+--EXTENSIONS--
+dom
+--FILE--
+loadXML('foo1');
+
+$nodes = $dom->documentElement->childNodes;
+$iter = $nodes->getIterator();
+
+var_dump($iter->valid());
+var_dump($iter->current()?->wholeText);
+$iter->next();
+var_dump($iter->valid());
+var_dump($iter->current()?->wholeText);
+
+?>
+--EXPECT--
+bool(true)
+string(4) "foo1"
+bool(false)
+NULL
diff --git a/ext/dom/tests/entity_reference_stale_01.phpt b/ext/dom/tests/entity_reference_stale_01.phpt
new file mode 100644
index 00000000000..dc1828c3cd9
--- /dev/null
+++ b/ext/dom/tests/entity_reference_stale_01.phpt
@@ -0,0 +1,41 @@
+--TEST--
+Entity references with stale entity declaration 01
+--EXTENSIONS--
+dom
+--FILE--
+loadXML(<<
+]>
+&foo;
+XML);
+
+$ref = $dom->documentElement->firstChild;
+$decl = $ref->firstChild;
+
+$nodes = $ref->childNodes;
+$dom->removeChild($dom->doctype);
+unset($decl);
+
+var_dump($nodes);
+var_dump($ref->firstChild);
+var_dump($ref->lastChild);
+var_dump($ref->textContent);
+var_dump($ref->childNodes);
+
+?>
+--EXPECT--
+object(DOMNodeList)#4 (1) {
+ ["length"]=>
+ int(0)
+}
+NULL
+NULL
+string(0) ""
+object(DOMNodeList)#2 (1) {
+ ["length"]=>
+ int(0)
+}
diff --git a/ext/dom/tests/entity_reference_stale_02.phpt b/ext/dom/tests/entity_reference_stale_02.phpt
new file mode 100644
index 00000000000..9e25a2c0e94
--- /dev/null
+++ b/ext/dom/tests/entity_reference_stale_02.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Entity references with stale entity declaration 02
+--EXTENSIONS--
+dom
+--FILE--
+loadXML(<<
+
+
+]>
+&foo1;
+XML);
+
+$ref = $dom->documentElement->firstChild;
+$decl = $ref->firstChild;
+
+$nodes = $ref->childNodes;
+$iter = $nodes->getIterator();
+$iter->next();
+$dom->removeChild($dom->doctype);
+unset($decl);
+
+try {
+ var_dump($iter->current()->publicId);
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+NULL