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