Skip to content

Commit 2f318cf

Browse files
committed
Implement DOMNode::isEqualNode()
Since we still support obsoleted nodes in our implementation, this uses the old spec to match the old nodes; and this uses the new spec for nodes still defined in the living spec. When unclear, the behaviour was cross-verified with Firefox. References: https://dom.spec.whatwg.org/#dom-node-isequalnode (for everything still in the living spec) https://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/DOM3-Core.html#core-Node3-isEqualNode (for old nodes removed from the living spec) Closes GH-11690.
1 parent c97507b commit 2f318cf

File tree

6 files changed

+520
-1
lines changed

6 files changed

+520
-1
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ PHP NEWS
2828
. Added DOMNode::isConnected and DOMNameSpaceNode::isConnected. (nielsdos)
2929
. Added DOMNode::parentElement and DOMNameSpaceNode::parentElement.
3030
(nielsdos)
31+
. Added DOMNode::isEqualNode(). (nielsdos)
3132

3233
- FPM:
3334
. Added warning to log when fpm socket was not registered on the expected

UPGRADING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ PHP 8.3 UPGRADE NOTES
270270
. Added DOMParentNode::replaceChildren().
271271
. Added DOMNode::isConnected and DOMNameSpaceNode::isConnected.
272272
. Added DOMNode::parentElement and DOMNameSpaceNode::parentElement.
273+
. Added DOMNode::isEqualNode().
273274

274275
- JSON:
275276
. Added json_validate(), which returns whether the json is valid for

ext/dom/node.c

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,155 @@ PHP_METHOD(DOMNode, isSameNode)
14711471
}
14721472
/* }}} end dom_node_is_same_node */
14731473

1474+
static bool php_dom_node_is_content_equal(const xmlNode *this, const xmlNode *other)
1475+
{
1476+
xmlChar *this_content = xmlNodeGetContent(this);
1477+
xmlChar *other_content = xmlNodeGetContent(other);
1478+
bool result = xmlStrEqual(this_content, other_content);
1479+
xmlFree(this_content);
1480+
xmlFree(other_content);
1481+
return result;
1482+
}
1483+
1484+
static bool php_dom_node_is_ns_uri_equal(const xmlNode *this, const xmlNode *other)
1485+
{
1486+
const xmlChar *this_ns = this->ns ? this->ns->href : NULL;
1487+
const xmlChar *other_ns = other->ns ? other->ns->href : NULL;
1488+
return xmlStrEqual(this_ns, other_ns);
1489+
}
1490+
1491+
static bool php_dom_node_is_ns_prefix_equal(const xmlNode *this, const xmlNode *other)
1492+
{
1493+
const xmlChar *this_ns = this->ns ? this->ns->prefix : NULL;
1494+
const xmlChar *other_ns = other->ns ? other->ns->prefix : NULL;
1495+
return xmlStrEqual(this_ns, other_ns);
1496+
}
1497+
1498+
static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other);
1499+
1500+
#define PHP_DOM_FUNC_CAT(prefix, suffix) prefix##_##suffix
1501+
/* xmlNode and xmlNs have incompatible struct layouts, i.e. the next field is in a different offset */
1502+
#define PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(type) \
1503+
static size_t PHP_DOM_FUNC_CAT(php_dom_node_count_list_size, type)(const type *node) \
1504+
{ \
1505+
size_t counter = 0; \
1506+
while (node) { \
1507+
counter++; \
1508+
node = node->next; \
1509+
} \
1510+
return counter; \
1511+
} \
1512+
static bool PHP_DOM_FUNC_CAT(php_dom_node_list_equality_check, type)(const type *list1, const type *list2) \
1513+
{ \
1514+
size_t count = PHP_DOM_FUNC_CAT(php_dom_node_count_list_size, type)(list1); \
1515+
if (count != PHP_DOM_FUNC_CAT(php_dom_node_count_list_size, type)(list2)) { \
1516+
return false; \
1517+
} \
1518+
for (size_t i = 0; i < count; i++) { \
1519+
if (!php_dom_node_is_equal_node((const xmlNode *) list1, (const xmlNode *) list2)) { \
1520+
return false; \
1521+
} \
1522+
list1 = list1->next; \
1523+
list2 = list2->next; \
1524+
} \
1525+
return true; \
1526+
}
1527+
PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(xmlNode)
1528+
PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(xmlNs)
1529+
1530+
static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other)
1531+
{
1532+
ZEND_ASSERT(this != NULL);
1533+
ZEND_ASSERT(other != NULL);
1534+
1535+
if (this->type != other->type) {
1536+
return false;
1537+
}
1538+
1539+
/* Notes:
1540+
* - XML_DOCUMENT_TYPE_NODE is no longer created by libxml2, we only have to support XML_DTD_NODE.
1541+
* - element and attribute declarations are not exposed as nodes in DOM, so no comparison is needed for those. */
1542+
if (this->type == XML_ELEMENT_NODE) {
1543+
return xmlStrEqual(this->name, other->name)
1544+
&& php_dom_node_is_ns_prefix_equal(this, other)
1545+
&& php_dom_node_is_ns_uri_equal(this, other)
1546+
/* Check attributes first, then namespace declarations, then children */
1547+
&& php_dom_node_list_equality_check_xmlNode((const xmlNode *) this->properties, (const xmlNode *) other->properties)
1548+
&& php_dom_node_list_equality_check_xmlNs(this->nsDef, other->nsDef)
1549+
&& php_dom_node_list_equality_check_xmlNode(this->children, other->children);
1550+
} else if (this->type == XML_DTD_NODE) {
1551+
/* Note: in the living spec entity declarations and notations are no longer compared because they're considered obsolete. */
1552+
const xmlDtd *this_dtd = (const xmlDtd *) this;
1553+
const xmlDtd *other_dtd = (const xmlDtd *) other;
1554+
return xmlStrEqual(this_dtd->name, other_dtd->name)
1555+
&& xmlStrEqual(this_dtd->ExternalID, other_dtd->ExternalID)
1556+
&& xmlStrEqual(this_dtd->SystemID, other_dtd->SystemID);
1557+
} else if (this->type == XML_PI_NODE) {
1558+
return xmlStrEqual(this->name, other->name) && xmlStrEqual(this->content, other->content);
1559+
} else if (this->type == XML_TEXT_NODE || this->type == XML_COMMENT_NODE || this->type == XML_CDATA_SECTION_NODE) {
1560+
return xmlStrEqual(this->content, other->content);
1561+
} else if (this->type == XML_ATTRIBUTE_NODE) {
1562+
const xmlAttr *this_attr = (const xmlAttr *) this;
1563+
const xmlAttr *other_attr = (const xmlAttr *) other;
1564+
return xmlStrEqual(this_attr->name, other_attr->name)
1565+
&& php_dom_node_is_ns_uri_equal(this, other)
1566+
&& php_dom_node_is_content_equal(this, other);
1567+
} else if (this->type == XML_ENTITY_REF_NODE) {
1568+
return xmlStrEqual(this->name, other->name);
1569+
} else if (this->type == XML_ENTITY_DECL || this->type == XML_NOTATION_NODE || this->type == XML_ENTITY_NODE) {
1570+
const xmlEntity *this_entity = (const xmlEntity *) this;
1571+
const xmlEntity *other_entity = (const xmlEntity *) other;
1572+
return this_entity->etype == other_entity->etype
1573+
&& xmlStrEqual(this_entity->name, other_entity->name)
1574+
&& xmlStrEqual(this_entity->ExternalID, other_entity->ExternalID)
1575+
&& xmlStrEqual(this_entity->SystemID, other_entity->SystemID)
1576+
&& php_dom_node_is_content_equal(this, other);
1577+
} else if (this->type == XML_NAMESPACE_DECL) {
1578+
const xmlNs *this_ns = (const xmlNs *) this;
1579+
const xmlNs *other_ns = (const xmlNs *) other;
1580+
return xmlStrEqual(this_ns->prefix, other_ns->prefix) && xmlStrEqual(this_ns->href, other_ns->href);
1581+
} else if (this->type == XML_DOCUMENT_FRAG_NODE || this->type == XML_HTML_DOCUMENT_NODE || this->type == XML_DOCUMENT_NODE) {
1582+
return php_dom_node_list_equality_check_xmlNode(this->children, other->children);
1583+
}
1584+
1585+
return false;
1586+
}
1587+
1588+
/* {{{ URL: https://dom.spec.whatwg.org/#dom-node-isequalnode (for everything still in the living spec)
1589+
* URL: https://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/DOM3-Core.html#core-Node3-isEqualNode (for old nodes removed from the living spec)
1590+
Since: DOM Level 3
1591+
*/
1592+
PHP_METHOD(DOMNode, isEqualNode)
1593+
{
1594+
zval *id, *node;
1595+
xmlNodePtr otherp, nodep;
1596+
dom_object *unused_intern;
1597+
1598+
id = ZEND_THIS;
1599+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "O!", &node, dom_node_class_entry) == FAILURE) {
1600+
RETURN_THROWS();
1601+
}
1602+
1603+
if (node == NULL) {
1604+
RETURN_FALSE;
1605+
}
1606+
1607+
DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, unused_intern);
1608+
DOM_GET_OBJ(otherp, node, xmlNodePtr, unused_intern);
1609+
1610+
if (nodep == otherp) {
1611+
RETURN_TRUE;
1612+
}
1613+
1614+
/* Empty fragments/documents only match if they're both empty */
1615+
if (UNEXPECTED(nodep == NULL || otherp == NULL)) {
1616+
RETURN_BOOL(nodep == NULL && otherp == NULL);
1617+
}
1618+
1619+
RETURN_BOOL(php_dom_node_is_equal_node(nodep, otherp));
1620+
}
1621+
/* }}} end DOMNode::isEqualNode */
1622+
14741623
/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#Node3-lookupNamespacePrefix
14751624
Since: DOM Level 3
14761625
*/

ext/dom/php_dom.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ public function isDefaultNamespace(string $namespace): bool {}
381381
/** @tentative-return-type */
382382
public function isSameNode(DOMNode $otherNode): bool {}
383383

384+
public function isEqualNode(?DOMNode $otherNode): bool {}
385+
384386
/** @tentative-return-type */
385387
public function isSupported(string $feature, string $version): bool {}
386388

ext/dom/php_dom_arginfo.h

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)