Fix bug #79701: getElementById does not correctly work with duplicate definitions

This is a long standing bug: IDs aren't properly tracked causing either
outdated or plain incorrect results from getElementById.

This PR implements a pragmatic solution in which we still try to use the
ID lookup table to a degree, but only as a performance boost not as a
"single source of truth". Full details are explained in the
getElementById code.

Closes GH-14349.
This commit is contained in:
Niels Dossche 2024-05-26 13:51:40 +02:00
parent 5fe799a4c6
commit 8dc2391bae
No known key found for this signature in database
GPG Key ID: B8A8AD166DF0E2E5
18 changed files with 672 additions and 41 deletions

2
NEWS
View File

@ -45,6 +45,8 @@ PHP NEWS
. Implemented "Improve callbacks in ext/dom and ext/xsl" RFC. (nielsdos)
. Added DOMXPath::quote() static method. (divinity76)
. Implemented opt-in ext/dom spec compliance RFC. (nielsdos)
. Fixed bug #79701 (getElementById does not correctly work with duplicate
definitions). (nielsdos)
- Fileinfo:
. Update to libmagic 5.45. (nielsdos)

View File

@ -25,6 +25,7 @@
#include "php_dom.h"
#include "dom_properties.h"
#include "internal_helpers.h"
/*
* class DOMAttr extends DOMNode
@ -106,6 +107,16 @@ zend_result dom_attr_specified_read(dom_object *obj, zval *retval)
/* }}} */
void dom_attr_value_will_change(dom_object *obj, xmlAttrPtr attrp)
{
if (attrp->atype == XML_ATTRIBUTE_ID) {
xmlRemoveID(attrp->doc, attrp);
attrp->atype = XML_ATTRIBUTE_ID;
}
dom_mark_ids_modified(obj->document);
}
/* {{{ value string
readonly=no
URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#ID-221662474
@ -122,6 +133,8 @@ zend_result dom_attr_value_write(dom_object *obj, zval *newval)
{
DOM_PROP_NODE(xmlAttrPtr, attrp, obj);
dom_attr_value_will_change(obj, attrp);
/* Typed property, this is already a string */
ZEND_ASSERT(Z_TYPE_P(newval) == IS_STRING);
zend_string *str = Z_STR_P(newval);

View File

@ -1028,34 +1028,47 @@ Since: DOM Level 2
*/
PHP_METHOD(DOMDocument, getElementById)
{
zval *id;
xmlDocPtr docp;
xmlAttrPtr attrp;
size_t idname_len;
dom_object *intern;
char *idname;
id = ZEND_THIS;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &idname, &idname_len) == FAILURE) {
RETURN_THROWS();
}
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(idname, idname_len)
ZEND_PARSE_PARAMETERS_END();
DOM_GET_OBJ(docp, id, xmlDocPtr, intern);
DOM_GET_OBJ(docp, ZEND_THIS, xmlDocPtr, intern);
attrp = xmlGetID(docp, BAD_CAST idname);
/* From the moment an ID is created, libxml2's behaviour is to cache that element, even
* if that element is not yet attached to the document. Similarly, only upon destruction of
* the element the ID is actually removed by libxml2. Since libxml2 has such behaviour deeply
* ingrained in the library, and uses the cache for various purposes, it seems like a bad
* idea and lost cause to fight it. Instead, we'll simply walk the tree upwards to check
* if the node is attached to the document. */
if (attrp && attrp->parent && php_dom_is_node_connected(attrp->parent)) {
DOM_RET_OBJ((xmlNodePtr) attrp->parent, intern);
/* If the document has not been manipulated yet, the ID cache will be in sync and we can trust its result.
* This check mainly exists because a lot of times people just query a document without modifying it,
* and we can allow quick access to IDs in that case. */
if (!dom_is_document_cache_modified_since_parsing(intern->document)) {
const xmlAttr *attrp = xmlGetID(docp, BAD_CAST idname);
if (attrp && attrp->parent) {
DOM_RET_OBJ(attrp->parent, intern);
}
} else {
RETVAL_NULL();
}
/* From the moment an ID is created, libxml2's behaviour is to cache that element, even
* if that element is not yet attached to the document. Similarly, only upon destruction of
* the element the ID is actually removed by libxml2. Since libxml2 has such behaviour deeply
* ingrained in the library, and uses the cache for various purposes, it seems like a bad
* idea and lost cause to fight it. */
const xmlNode *base = (const xmlNode *) docp;
const xmlNode *node = base->children;
while (node != NULL) {
if (node->type == XML_ELEMENT_NODE) {
for (const xmlAttr *attr = node->properties; attr != NULL; attr = attr->next) {
if (attr->atype == XML_ATTRIBUTE_ID && dom_compare_value(attr, BAD_CAST idname)) {
DOM_RET_OBJ((xmlNodePtr) node, intern);
return;
}
}
}
node = php_dom_next_in_tree_order(node, base);
}
}
}
/* }}} end dom_document_get_element_by_id */

View File

@ -183,7 +183,7 @@ zend_result dom_element_id_read(dom_object *obj, zval *retval)
return dom_element_reflected_attribute_read(obj, retval, "id");
}
static void php_set_attribute_id(xmlAttrPtr attrp, bool is_id);
static void php_set_attribute_id(xmlAttrPtr attrp, bool is_id, php_libxml_ref_obj *document);
zend_result dom_element_id_write(dom_object *obj, zval *newval)
{
@ -191,7 +191,7 @@ zend_result dom_element_id_write(dom_object *obj, zval *newval)
if (!attr) {
return FAILURE;
}
php_set_attribute_id(attr, true);
php_set_attribute_id(attr, true, obj->document);
return SUCCESS;
}
/* }}} */
@ -352,6 +352,16 @@ static xmlNodePtr dom_create_attribute(xmlNodePtr nodep, const char *name, const
}
}
static void dom_check_register_attribute_id(xmlAttrPtr attr, php_libxml_ref_obj *document)
{
dom_mark_ids_modified(document);
if (attr->atype != XML_ATTRIBUTE_ID && attr->doc->type == XML_HTML_DOCUMENT_NODE && attr->ns == NULL && xmlStrEqual(attr->name, BAD_CAST "id")) {
/* To respect XML's ID behaviour, we only do this registration for HTML documents. */
attr->atype = XML_ATTRIBUTE_ID;
}
}
/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#core-ID-F68F082
Modern spec URL: https://dom.spec.whatwg.org/#dom-element-setattribute
Since:
@ -360,7 +370,6 @@ PHP_METHOD(DOMElement, setAttribute)
{
zval *id;
xmlNode *nodep;
xmlNodePtr attr = NULL;
int name_valid;
size_t name_len, value_len;
dom_object *intern;
@ -394,23 +403,28 @@ PHP_METHOD(DOMElement, setAttribute)
}
/* Can't use xmlSetNsProp unconditionally here because that doesn't take into account the qualified name matching... */
attr = (xmlNodePtr) php_dom_get_attribute_node(nodep, BAD_CAST name, name_len);
xmlAttrPtr attr = php_dom_get_attribute_node(nodep, BAD_CAST name, name_len);
if (attr != NULL) {
dom_remove_all_children(attr);
dom_attr_value_will_change(intern, attr);
dom_remove_all_children((xmlNodePtr) attr);
xmlNodePtr node = xmlNewDocText(attr->doc, BAD_CAST value);
xmlAddChild(attr, node);
xmlAddChild((xmlNodePtr) attr, node);
} else {
attr = (xmlNodePtr) xmlSetNsProp(nodep, NULL, name_processed, BAD_CAST value);
attr = xmlSetNsProp(nodep, NULL, name_processed, BAD_CAST value);
if (EXPECTED(attr != NULL)) {
dom_check_register_attribute_id(attr, intern->document);
}
}
if (name_processed != BAD_CAST name) {
efree(name_processed);
}
} else {
attr = dom_get_attribute_or_nsdecl(intern, nodep, BAD_CAST name, name_len);
xmlNodePtr attr = dom_get_attribute_or_nsdecl(intern, nodep, BAD_CAST name, name_len);
if (attr != NULL) {
switch (attr->type) {
case XML_ATTRIBUTE_NODE:
dom_attr_value_will_change(intern, (xmlAttrPtr) attr);
node_list_unlink(attr->children);
break;
case XML_NAMESPACE_DECL:
@ -693,7 +707,10 @@ static void dom_element_set_attribute_node_common(INTERNAL_FUNCTION_PARAMETERS,
xmlAddChild(nodep, (xmlNodePtr) attrp);
if (!modern) {
dom_mark_ids_modified(intern->document);
php_dom_reconcile_attribute_namespace_after_insertion(attrp);
} else {
dom_check_register_attribute_id(attrp, intern->document);
}
/* Returns old property if removed otherwise NULL */
@ -870,6 +887,8 @@ static void dom_set_attribute_ns_legacy(dom_object *intern, xmlNodePtr elemp, ch
int errorcode = dom_check_qname(name, &localname, &prefix, uri_len, name_len);
if (errorcode == 0) {
dom_mark_ids_modified(intern->document);
if (uri_len > 0) {
nodep = (xmlNodePtr) xmlHasNsProp(elemp, BAD_CAST localname, BAD_CAST uri);
if (nodep != NULL && nodep->type != XML_ATTRIBUTE_DECL) {
@ -958,8 +977,11 @@ static void dom_set_attribute_ns_modern(dom_object *intern, xmlNodePtr elemp, ze
if (errorcode == 0) {
php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(intern);
xmlNsPtr ns = php_dom_libxml_ns_mapper_get_ns_raw_prefix_string(ns_mapper, prefix, xmlStrlen(prefix), uri);
if (UNEXPECTED(xmlSetNsProp(elemp, ns, localname, BAD_CAST value) == NULL)) {
xmlAttrPtr attr = xmlSetNsProp(elemp, ns, localname, BAD_CAST value);
if (UNEXPECTED(attr == NULL)) {
php_dom_throw_error(INVALID_STATE_ERR, /* strict */ true);
} else {
dom_check_register_attribute_id(attr, intern->document);
}
} else {
php_dom_throw_error(errorcode, /* strict */ true);
@ -1270,20 +1292,16 @@ PHP_METHOD(DOMElement, hasAttributeNS)
}
/* }}} end dom_element_has_attribute_ns */
static void php_set_attribute_id(xmlAttrPtr attrp, bool is_id) /* {{{ */
static void php_set_attribute_id(xmlAttrPtr attrp, bool is_id, php_libxml_ref_obj *document) /* {{{ */
{
if (is_id == 1 && attrp->atype != XML_ATTRIBUTE_ID) {
xmlChar *id_val;
id_val = xmlNodeListGetString(attrp->doc, attrp->children, 1);
if (id_val != NULL) {
xmlAddID(NULL, attrp->doc, id_val, attrp);
xmlFree(id_val);
}
} else if (is_id == 0 && attrp->atype == XML_ATTRIBUTE_ID) {
if (is_id && attrp->atype != XML_ATTRIBUTE_ID) {
attrp->atype = XML_ATTRIBUTE_ID;
} else if (!is_id && attrp->atype == XML_ATTRIBUTE_ID) {
xmlRemoveID(attrp->doc, attrp);
attrp->atype = 0;
}
dom_mark_ids_modified(document);
}
/* }}} */
@ -1311,7 +1329,7 @@ PHP_METHOD(DOMElement, setIdAttribute)
if (attrp == NULL || attrp->type == XML_ATTRIBUTE_DECL) {
php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(intern->document));
} else {
php_set_attribute_id(attrp, is_id);
php_set_attribute_id(attrp, is_id, intern->document);
}
}
/* }}} end dom_element_set_id_attribute */
@ -1340,7 +1358,7 @@ PHP_METHOD(DOMElement, setIdAttributeNS)
if (attrp == NULL || attrp->type == XML_ATTRIBUTE_DECL) {
php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(intern->document));
} else {
php_set_attribute_id(attrp, is_id);
php_set_attribute_id(attrp, is_id, intern->document);
}
}
/* }}} end dom_element_set_id_attribute_ns */
@ -1367,7 +1385,7 @@ static void dom_element_set_id_attribute_node(INTERNAL_FUNCTION_PARAMETERS, zend
if (attrp->parent != nodep) {
php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(intern->document));
} else {
php_set_attribute_id(attrp, is_id);
php_set_attribute_id(attrp, is_id, intern->document);
}
}

View File

@ -63,4 +63,31 @@ DOM_DEF_GET_CE_FUNC(domimplementation)
#endif
static zend_always_inline size_t dom_minimum_modification_nr_since_parsing(php_libxml_ref_obj *doc_ptr)
{
/* For old-style DOM, we need a "new DOMDocument" + a load, so the minimum modification nr is 2.
* For new-style DOM, we need only to call a named constructor, so the minimum modification nr is 1. */
return doc_ptr->class_type == PHP_LIBXML_CLASS_MODERN ? 1 : 2;
}
static zend_always_inline void dom_mark_document_cache_as_modified_since_parsing(php_libxml_ref_obj *doc_ptr)
{
if (doc_ptr) {
doc_ptr->cache_tag.modification_nr = MAX(dom_minimum_modification_nr_since_parsing(doc_ptr) + 1,
doc_ptr->cache_tag.modification_nr);
}
}
/* Marks the ID cache as potentially stale */
static zend_always_inline void dom_mark_ids_modified(php_libxml_ref_obj *doc_ptr)
{
/* Although this is currently a wrapper function, it's best to abstract the actual implementation away. */
dom_mark_document_cache_as_modified_since_parsing(doc_ptr);
}
static zend_always_inline bool dom_is_document_cache_modified_since_parsing(php_libxml_ref_obj *doc_ptr)
{
return !doc_ptr || doc_ptr->cache_tag.modification_nr > dom_minimum_modification_nr_since_parsing(doc_ptr);
}
#endif

View File

@ -185,6 +185,7 @@ zend_result dom_node_node_value_write(dom_object *obj, zval *newval)
/* Access to Element node is implemented as a convenience method */
switch (nodep->type) {
case XML_ATTRIBUTE_NODE:
dom_attr_value_will_change(obj, (xmlAttrPtr) nodep);
if (php_dom_follow_spec_intern(obj)) {
dom_remove_all_children(nodep);
xmlAddChild(nodep, xmlNewTextLen(BAD_CAST ZSTR_VAL(str), ZSTR_LEN(str)));

View File

@ -174,6 +174,7 @@ xmlDocPtr php_dom_create_html_doc(void);
xmlEntityPtr dom_entity_reference_fetch_and_sync_declaration(xmlNodePtr reference);
void dom_set_xml_class(php_libxml_ref_obj *document);
bool dom_compare_value(const xmlAttr *attr, const xmlChar *value);
void dom_attr_value_will_change(dom_object *obj, xmlAttrPtr attrp);
typedef enum {
DOM_LOAD_STRING = 0,

View File

@ -0,0 +1,53 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - id property variation
--EXTENSIONS--
dom
--FILE--
<?php
$dom = Dom\XMLDocument::createFromString(<<<XML
<root>
<test1/>
<test2/>
</root>
XML);
$test1 = $dom->documentElement->firstElementChild;
$test2 = $test1->nextElementSibling;
echo "--- After parsing ---\n";
var_dump($dom->getElementById("x")?->nodeName);
echo "--- After setting test2 ---\n";
$test2->id = "x";
var_dump($dom->getElementById("x")?->nodeName);
echo "--- After setting test1 ---\n";
$test1->id = "x";
var_dump($dom->getElementById("x")?->nodeName);
echo "--- After resetting test1 ---\n";
$test1->id = "y";
var_dump($dom->getElementById("x")?->nodeName);
echo "--- After resetting test2 ---\n";
$test2->id = "y";
var_dump($dom->getElementById("x")?->nodeName);
echo "--- After resetting test1 ---\n";
$test1->id = "x";
var_dump($dom->getElementById("x")?->nodeName);
echo "--- After calling setIdAttribute with false on test1 ---\n";
$test1->setIdAttribute("id", false);
var_dump($dom->getElementById("x")?->nodeName);
?>
--EXPECT--
--- After parsing ---
NULL
--- After setting test2 ---
string(5) "test2"
--- After setting test1 ---
string(5) "test1"
--- After resetting test1 ---
string(5) "test2"
--- After resetting test2 ---
NULL
--- After resetting test1 ---
string(5) "test1"
--- After calling setIdAttribute with false on test1 ---
NULL

View File

@ -0,0 +1,18 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - attribute node variation
--EXTENSIONS--
dom
--FILE--
<?php
$dom = Dom\HTMLDocument::createEmpty();
$element = $dom->createElement('foo');
$dom->append($element);
$attr = $dom->createAttribute('id');
$attr->value = 'test';
var_dump($dom->getElementById('test')?->nodeName);
$element->setAttributeNode($attr);
var_dump($dom->getElementById('test')?->nodeName);
?>
--EXPECT--
NULL
string(3) "FOO"

View File

@ -0,0 +1,29 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - prepending variation
--EXTENSIONS--
dom
--FILE--
<?php
$dom = new DOMDocument();
$root = $dom->createElement('html');
$dom->appendChild($root);
$el1 = $dom->createElement('p1');
$el1->setAttribute('id', 'foo');
$el1->setIdAttribute('id', true);
$root->appendChild($el1);
$el2 = $dom->createElement('p2');
$el2->setAttribute('id', 'foo');
$el2->setIdAttribute('id', true);
$root->appendChild($el2);
unset($el1, $el2);
$root->removeChild($dom->getElementById('foo'));
var_dump($dom->getElementById('foo')?->nodeName);
?>
--EXPECT--
string(2) "p2"

View File

@ -0,0 +1,21 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - remove attribute variation
--EXTENSIONS--
dom
--FILE--
<?php
$dom = Dom\XMLDocument::createFromString(<<<XML
<root>
<test1 xml:id="x"/>
<test2 xml:id="x"/>
</root>
XML);
var_dump($dom->getElementById('x')?->nodeName);
$dom->getElementById('x')->removeAttribute('xml:id');
var_dump($dom->getElementById('x')?->nodeName);
?>
--EXPECTF--
Warning: Dom\XMLDocument::createFromString(): ID x already defined in Entity, line: 3 in %s on line %d
string(5) "test1"
NULL

View File

@ -0,0 +1,45 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - nodeValue / value variation
--EXTENSIONS--
dom
--FILE--
<?php
foreach (["value", "nodeValue"] as $property) {
echo "--- Testing property \$$property ---\n";
$dom = Dom\XMLDocument::createFromString(<<<XML
<root>
<test1 xml:id="x"/>
<test2 xml:id="y"/>
</root>
XML);
$test1 = $dom->getElementById('x');
$test2 = $dom->getElementById('y');
var_dump($test1?->nodeName);
var_dump($test2?->nodeName);
$test1->getAttributeNode('xml:id')->$property = "y";
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
$test2->getAttributeNode('xml:id')->$property = "x";
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
}
?>
--EXPECT--
--- Testing property $value ---
string(5) "test1"
string(5) "test2"
NULL
string(5) "test1"
string(5) "test2"
string(5) "test1"
--- Testing property $nodeValue ---
string(5) "test1"
string(5) "test2"
NULL
string(5) "test1"
string(5) "test2"
string(5) "test1"

View File

@ -0,0 +1,82 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - set attribute in html variation
--EXTENSIONS--
dom
--FILE--
<?php
foreach (["xml:id", "ID"] as $name) {
$dom = Dom\HTMLDocument::createFromString(<<<HTML
<p>
<em id="x">1</em>
<strong id="y">2</strong>
</p>
HTML, LIBXML_NOERROR);
echo "\n=== Attribute name $name ===\n\n";
$test1 = $dom->getElementById('x');
$test2 = $dom->getElementById('y');
echo "--- After resetting em's id ---\n";
$test1->setAttribute($name, 'y');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- After resetting strong's id ---\n";
$test2->setAttribute($name, 'x');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- After resetting em's id ---\n";
$test1->setAttribute($name, 'z');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- After resetting strong's id ---\n";
$test2->setAttribute($name, 'z');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- Get id z ---\n";
var_dump($dom->getElementById('z')?->nodeName);
}
?>
--EXPECT--
=== Attribute name xml:id ===
--- After resetting em's id ---
string(2) "EM"
string(6) "STRONG"
--- After resetting strong's id ---
string(2) "EM"
string(6) "STRONG"
--- After resetting em's id ---
string(2) "EM"
string(6) "STRONG"
--- After resetting strong's id ---
string(2) "EM"
string(6) "STRONG"
--- Get id z ---
NULL
=== Attribute name ID ===
--- After resetting em's id ---
NULL
string(2) "EM"
--- After resetting strong's id ---
string(6) "STRONG"
string(2) "EM"
--- After resetting em's id ---
string(6) "STRONG"
NULL
--- After resetting strong's id ---
NULL
NULL
--- Get id z ---
string(2) "EM"

View File

@ -0,0 +1,85 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - set attribute ns in html variation
--EXTENSIONS--
dom
--FILE--
<?php
function test($namespace) {
$dom = Dom\HTMLDocument::createFromString(<<<HTML
<p>
<em id="x">1</em>
<strong id="y">2</strong>
</p>
HTML, LIBXML_NOERROR);
$test1 = $dom->getElementById('x');
$test2 = $dom->getElementById('y');
echo "--- After resetting em's id ---\n";
$test1->setAttributeNS($namespace, "id", 'y');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- After resetting strong's id ---\n";
$test2->setAttributeNS($namespace, "id", 'x');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- After resetting em's id ---\n";
$test1->setAttributeNS($namespace, "id", 'z');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- After resetting strong's id ---\n";
$test2->setAttributeNS($namespace, "id", 'z');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- Get id z ---\n";
var_dump($dom->getElementById('z')?->nodeName);
}
echo "=== Test empty namespace ===\n\n";
test("");
echo "\n=== Test \"urn:x\" namespace ===\n\n";
test("urn:x");
?>
--EXPECT--
=== Test empty namespace ===
--- After resetting em's id ---
NULL
string(2) "EM"
--- After resetting strong's id ---
string(6) "STRONG"
string(2) "EM"
--- After resetting em's id ---
string(6) "STRONG"
NULL
--- After resetting strong's id ---
NULL
NULL
--- Get id z ---
string(2) "EM"
=== Test "urn:x" namespace ===
--- After resetting em's id ---
string(2) "EM"
string(6) "STRONG"
--- After resetting strong's id ---
string(2) "EM"
string(6) "STRONG"
--- After resetting em's id ---
string(2) "EM"
string(6) "STRONG"
--- After resetting strong's id ---
string(2) "EM"
string(6) "STRONG"
--- Get id z ---
NULL

View File

@ -0,0 +1,143 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - set attribute in xml variation
--EXTENSIONS--
dom
--FILE--
<?php
function test($dom, $fn) {
$test1 = $dom->getElementById('x');
$test2 = $dom->getElementById('y');
echo "--- After resetting test1's id ---\n";
$fn($test1, 'xml:id', 'y');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- After resetting test2's id ---\n";
$fn($test2, 'xml:id', 'x');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- After resetting test1's id ---\n";
$fn($test1, 'xml:id', 'z');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- After resetting test2's id ---\n";
$fn($test2, 'xml:id', 'z');
var_dump($dom->getElementById('x')?->nodeName);
var_dump($dom->getElementById('y')?->nodeName);
echo "--- Get id z ---\n";
var_dump($dom->getElementById('z')?->nodeName);
}
function getNamespace($name) {
if (str_contains($name, 'xml:')) {
return 'http://www.w3.org/XML/1998/namespace';
}
return '';
}
$common_xml = <<<XML
<root>
<test1 xml:id="x"/>
<test2 xml:id="y"/>
</root>
XML;
echo "\n=== DOMDocument: setAttribute ===\n\n";
$dom = new DOMDocument;
$dom->loadXML($common_xml);
test($dom, fn ($element, $name, $value) => $element->setAttribute($name, $value));
echo "\n=== DOMDocument: setAttributeNS ===\n\n";
$dom = new DOMDocument;
$dom->loadXML($common_xml);
test($dom, fn ($element, $name, $value) => $element->setAttributeNS(getNamespace($name), $name, $value));
echo "\n=== Dom\\XMLDocument: setAttribute ===\n\n";
$dom = Dom\XMLDocument::createFromString($common_xml);
test($dom, fn ($element, $name, $value) => $element->setAttribute($name, $value));
echo "\n=== Dom\\XMLDocument: setAttributeNS ===\n\n";
$dom = Dom\XMLDocument::createFromString($common_xml);
test($dom, fn ($element, $name, $value) => $element->setAttributeNS(getNamespace($name), $name, $value));
?>
--EXPECT--
=== DOMDocument: setAttribute ===
--- After resetting test1's id ---
NULL
string(5) "test1"
--- After resetting test2's id ---
string(5) "test2"
string(5) "test1"
--- After resetting test1's id ---
string(5) "test2"
NULL
--- After resetting test2's id ---
NULL
NULL
--- Get id z ---
string(5) "test1"
=== DOMDocument: setAttributeNS ===
--- After resetting test1's id ---
NULL
string(5) "test1"
--- After resetting test2's id ---
string(5) "test2"
string(5) "test1"
--- After resetting test1's id ---
string(5) "test2"
NULL
--- After resetting test2's id ---
NULL
NULL
--- Get id z ---
string(5) "test1"
=== Dom\XMLDocument: setAttribute ===
--- After resetting test1's id ---
NULL
string(5) "test1"
--- After resetting test2's id ---
string(5) "test2"
string(5) "test1"
--- After resetting test1's id ---
string(5) "test2"
NULL
--- After resetting test2's id ---
NULL
NULL
--- Get id z ---
string(5) "test1"
=== Dom\XMLDocument: setAttributeNS ===
--- After resetting test1's id ---
NULL
string(5) "test1"
--- After resetting test2's id ---
string(5) "test2"
string(5) "test1"
--- After resetting test1's id ---
string(5) "test2"
NULL
--- After resetting test2's id ---
NULL
NULL
--- Get id z ---
string(5) "test1"

View File

@ -0,0 +1,47 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - swapping variation
--EXTENSIONS--
dom
--FILE--
<?php
$dom = new DOMDocument;
$dom->loadXML(<<<XML
<root>
<test1 xml:id="x"/>
<test2 attr="x"/>
</root>
XML);
$test1 = $dom->getElementById('x');
echo "--- After parsing ---\n";
var_dump($dom->getElementById('x')?->nodeName);
echo "--- After adding a new id attribute ---\n";
$dom->getElementsByTagName('test2')[0]->setIdAttribute('attr', true);
var_dump($dom->getElementById('x')?->nodeName);
echo "--- After removing the first id ---\n";
$dom->getElementById('x')->remove();
var_dump($dom->getElementById('x')?->nodeName);
echo "--- After removing the second id ---\n";
$dom->getElementById('x')->remove();
var_dump($dom->getElementById('x')?->nodeName);
echo "--- After re-adding the first id ---\n";
$dom->documentElement->appendChild($test1);
var_dump($dom->getElementById('x')?->nodeName);
echo "--- After changing the first id ---\n";
$test1->setAttribute('xml:id', 'y');
var_dump($dom->getElementById('x')?->nodeName);
?>
--EXPECT--
--- After parsing ---
string(5) "test1"
--- After adding a new id attribute ---
string(5) "test1"
--- After removing the first id ---
string(5) "test2"
--- After removing the second id ---
NULL
--- After re-adding the first id ---
string(5) "test1"
--- After changing the first id ---
NULL

View File

@ -0,0 +1,17 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - toggle variation
--EXTENSIONS--
dom
--FILE--
<?php
$dom = Dom\HTMLDocument::createFromString('<p id="test">foo</p>', LIBXML_NOERROR | LIBXML_HTML_NOIMPLIED);
var_dump($dom->getElementById('test')?->nodeName);
$dom->documentElement->toggleAttribute('id');
var_dump($dom->getElementById('test')?->nodeName);
$dom->documentElement->toggleAttribute('id');
var_dump($dom->getElementById('test')?->nodeName);
?>
--EXPECT--
string(1) "P"
NULL
NULL

View File

@ -0,0 +1,16 @@
--TEST--
Bug #79701 (getElementById does not correctly work with duplicate definitions) - unconnected variation
--EXTENSIONS--
dom
--FILE--
<?php
$dom = Dom\HTMLDocument::createEmpty();
$element = $dom->createElement('foo');
$element->setAttribute('id', 'test');
var_dump($dom->getElementById('test')?->nodeName);
$dom->append($element);
var_dump($dom->getElementById('test')?->nodeName);
?>
--EXPECT--
NULL
string(3) "FOO"