Skip to content

Commit 65ff82f

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)
1 parent b24b351 commit 65ff82f

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
@@ -20,6 +20,7 @@ PHP NEWS
2020
. Added DOMElement::getAttributeNames(). (nielsdos)
2121
. Added DOMNode::getRootNode(). (nielsdos)
2222
. Added DOMElement::className. (nielsdos)
23+
. Added DOMNode::isEqualNode(). (nielsdos)
2324

2425
- Intl:
2526
. Fix memory leak in MessageFormatter::format() on failure. (Girgias)

UPGRADING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ PHP 8.3 UPGRADE NOTES
254254
yet.
255255
. Added DOMElement::className. This is not binary-safe at the moment
256256
because of underlying limitations of libxml2.
257+
. Added DOMNode::isEqualNode().
257258

258259
- JSON:
259260
. 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
@@ -1425,6 +1425,155 @@ PHP_METHOD(DOMNode, isSameNode)
14251425
}
14261426
/* }}} end dom_node_is_same_node */
14271427

1428+
static bool php_dom_node_is_content_equal(const xmlNode *this, const xmlNode *other)
1429+
{
1430+
xmlChar *this_content = xmlNodeGetContent(this);
1431+
xmlChar *other_content = xmlNodeGetContent(other);
1432+
bool result = xmlStrEqual(this_content, other_content);
1433+
xmlFree(this_content);
1434+
xmlFree(other_content);
1435+
return result;
1436+
}
1437+
1438+
static bool php_dom_node_is_ns_uri_equal(const xmlNode *this, const xmlNode *other)
1439+
{
1440+
const xmlChar *this_ns = this->ns ? this->ns->href : NULL;
1441+
const xmlChar *other_ns = other->ns ? other->ns->href : NULL;
1442+
return xmlStrEqual(this_ns, other_ns);
1443+
}
1444+
1445+
static bool php_dom_node_is_ns_prefix_equal(const xmlNode *this, const xmlNode *other)
1446+
{
1447+
const xmlChar *this_ns = this->ns ? this->ns->prefix : NULL;
1448+
const xmlChar *other_ns = other->ns ? other->ns->prefix : NULL;
1449+
return xmlStrEqual(this_ns, other_ns);
1450+
}
1451+
1452+
static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other);
1453+
1454+
#define PHP_DOM_FUNC_CAT(prefix, suffix) prefix##_##suffix
1455+
/* xmlNode and xmlNs have incompatible struct layouts, i.e. the next field is in a different offset */
1456+
#define PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(type) \
1457+
static size_t PHP_DOM_FUNC_CAT(php_dom_node_count_list_size, type)(const type *node) \
1458+
{ \
1459+
size_t counter = 0; \
1460+
while (node) { \
1461+
counter++; \
1462+
node = node->next; \
1463+
} \
1464+
return counter; \
1465+
} \
1466+
static bool PHP_DOM_FUNC_CAT(php_dom_node_list_equality_check, type)(const type *list1, const type *list2) \
1467+
{ \
1468+
size_t count = PHP_DOM_FUNC_CAT(php_dom_node_count_list_size, type)(list1); \
1469+
if (count != PHP_DOM_FUNC_CAT(php_dom_node_count_list_size, type)(list2)) { \
1470+
return false; \
1471+
} \
1472+
for (size_t i = 0; i < count; i++) { \
1473+
if (!php_dom_node_is_equal_node((const xmlNode *) list1, (const xmlNode *) list2)) { \
1474+
return false; \
1475+
} \
1476+
list1 = list1->next; \
1477+
list2 = list2->next; \
1478+
} \
1479+
return true; \
1480+
}
1481+
PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(xmlNode)
1482+
PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(xmlNs)
1483+
1484+
static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other)
1485+
{
1486+
ZEND_ASSERT(this != NULL);
1487+
ZEND_ASSERT(other != NULL);
1488+
1489+
if (this->type != other->type) {
1490+
return false;
1491+
}
1492+
1493+
/* Notes:
1494+
* - XML_DOCUMENT_TYPE_NODE is no longer created by libxml2, we only have to support XML_DTD_NODE.
1495+
* - element and attribute declarations are not exposed as nodes in DOM, so no comparison is needed for those. */
1496+
if (this->type == XML_ELEMENT_NODE) {
1497+
return xmlStrEqual(this->name, other->name)
1498+
&& php_dom_node_is_ns_prefix_equal(this, other)
1499+
&& php_dom_node_is_ns_uri_equal(this, other)
1500+
/* Check attributes first, then namespace declarations, then children */
1501+
&& php_dom_node_list_equality_check_xmlNode((const xmlNode *) this->properties, (const xmlNode *) other->properties)
1502+
&& php_dom_node_list_equality_check_xmlNs(this->nsDef, other->nsDef)
1503+
&& php_dom_node_list_equality_check_xmlNode(this->children, other->children);
1504+
} else if (this->type == XML_DTD_NODE) {
1505+
/* Note: in the living spec entity declarations and notations are no longer compared because they're considered obsolete. */
1506+
const xmlDtd *this_dtd = (const xmlDtd *) this;
1507+
const xmlDtd *other_dtd = (const xmlDtd *) other;
1508+
return xmlStrEqual(this_dtd->name, other_dtd->name)
1509+
&& xmlStrEqual(this_dtd->ExternalID, other_dtd->ExternalID)
1510+
&& xmlStrEqual(this_dtd->SystemID, other_dtd->SystemID);
1511+
} else if (this->type == XML_PI_NODE) {
1512+
return xmlStrEqual(this->name, other->name) && xmlStrEqual(this->content, other->content);
1513+
} else if (this->type == XML_TEXT_NODE || this->type == XML_COMMENT_NODE || this->type == XML_CDATA_SECTION_NODE) {
1514+
return xmlStrEqual(this->content, other->content);
1515+
} else if (this->type == XML_ATTRIBUTE_NODE) {
1516+
const xmlAttr *this_attr = (const xmlAttr *) this;
1517+
const xmlAttr *other_attr = (const xmlAttr *) other;
1518+
return xmlStrEqual(this_attr->name, other_attr->name)
1519+
&& php_dom_node_is_ns_uri_equal(this, other)
1520+
&& php_dom_node_is_content_equal(this, other);
1521+
} else if (this->type == XML_ENTITY_REF_NODE) {
1522+
return xmlStrEqual(this->name, other->name);
1523+
} else if (this->type == XML_ENTITY_DECL || this->type == XML_NOTATION_NODE || this->type == XML_ENTITY_NODE) {
1524+
const xmlEntity *this_entity = (const xmlEntity *) this;
1525+
const xmlEntity *other_entity = (const xmlEntity *) other;
1526+
return this_entity->etype == other_entity->etype
1527+
&& xmlStrEqual(this_entity->name, other_entity->name)
1528+
&& xmlStrEqual(this_entity->ExternalID, other_entity->ExternalID)
1529+
&& xmlStrEqual(this_entity->SystemID, other_entity->SystemID)
1530+
&& php_dom_node_is_content_equal(this, other);
1531+
} else if (this->type == XML_NAMESPACE_DECL) {
1532+
const xmlNs *this_ns = (const xmlNs *) this;
1533+
const xmlNs *other_ns = (const xmlNs *) other;
1534+
return xmlStrEqual(this_ns->prefix, other_ns->prefix) && xmlStrEqual(this_ns->href, other_ns->href);
1535+
} else if (this->type == XML_DOCUMENT_FRAG_NODE || this->type == XML_HTML_DOCUMENT_NODE || this->type == XML_DOCUMENT_NODE) {
1536+
return php_dom_node_list_equality_check_xmlNode(this->children, other->children);
1537+
}
1538+
1539+
return false;
1540+
}
1541+
1542+
/* {{{ URL: https://dom.spec.whatwg.org/#dom-node-isequalnode (for everything still in the living spec)
1543+
* 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)
1544+
Since: DOM Level 3
1545+
*/
1546+
PHP_METHOD(DOMNode, isEqualNode)
1547+
{
1548+
zval *id, *node;
1549+
xmlNodePtr otherp, nodep;
1550+
dom_object *unused_intern;
1551+
1552+
id = ZEND_THIS;
1553+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "O!", &node, dom_node_class_entry) == FAILURE) {
1554+
RETURN_THROWS();
1555+
}
1556+
1557+
if (node == NULL) {
1558+
RETURN_FALSE;
1559+
}
1560+
1561+
DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, unused_intern);
1562+
DOM_GET_OBJ(otherp, node, xmlNodePtr, unused_intern);
1563+
1564+
if (nodep == otherp) {
1565+
RETURN_TRUE;
1566+
}
1567+
1568+
/* Empty fragments/documents only match if they're both empty */
1569+
if (UNEXPECTED(nodep == NULL || otherp == NULL)) {
1570+
RETURN_BOOL(nodep == NULL && otherp == NULL);
1571+
}
1572+
1573+
RETURN_BOOL(php_dom_node_is_equal_node(nodep, otherp));
1574+
}
1575+
/* }}} end DOMNode::isEqualNode */
1576+
14281577
/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#Node3-lookupNamespacePrefix
14291578
Since: DOM Level 3
14301579
*/

ext/dom/php_dom.stub.php

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

375+
public function isEqualNode(?DOMNode $otherNode): bool {}
376+
375377
/** @tentative-return-type */
376378
public function isSupported(string $feature, string $version): bool {}
377379

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)