Skip to content

Commit 6cbdf07

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 ea794e9 commit 6cbdf07

File tree

6 files changed

+508
-1
lines changed

6 files changed

+508
-1
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ PHP NEWS
1616

1717
- DOM:
1818
. Added DOMNode::contains() and DOMNameSpaceNode::contains(). (nielsdos)
19+
. Added DOMNode::isEqualNode(). (nielsdos)
1920

2021
- Intl:
2122
. Fix memory leak in MessageFormatter::format() on failure. (Girgias)

UPGRADING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ PHP 8.3 UPGRADE NOTES
240240

241241
- DOM:
242242
. Added DOMNode::contains() and DOMNameSpaceNode::contains().
243+
. Added DOMNode::isEqualNode().
243244

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

ext/dom/node.c

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,162 @@ 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+
/* Note: spec doesn't explicitly mention it, but JS also checks equality of content for CDATA. */
1515+
return xmlStrEqual(this->content, other->content);
1516+
} else if (this->type == XML_ATTRIBUTE_NODE) {
1517+
const xmlAttr *this_attr = (const xmlAttr *) this;
1518+
const xmlAttr *other_attr = (const xmlAttr *) other;
1519+
return xmlStrEqual(this_attr->name, other_attr->name)
1520+
&& php_dom_node_is_ns_uri_equal(this, other)
1521+
&& php_dom_node_is_content_equal(this, other);
1522+
} else if (this->type == XML_ENTITY_REF_NODE) {
1523+
return xmlStrEqual(this->name, other->name);
1524+
} else if (this->type == XML_ENTITY_DECL || this->type == XML_NOTATION_NODE || this->type == XML_ENTITY_NODE) {
1525+
const xmlEntity *this_entity = (const xmlEntity *) this;
1526+
const xmlEntity *other_entity = (const xmlEntity *) other;
1527+
return this_entity->etype == other_entity->etype
1528+
&& xmlStrEqual(this_entity->name, other_entity->name)
1529+
&& xmlStrEqual(this_entity->ExternalID, other_entity->ExternalID)
1530+
&& xmlStrEqual(this_entity->SystemID, other_entity->SystemID)
1531+
&& php_dom_node_is_content_equal(this, other);
1532+
} else if (this->type == XML_NAMESPACE_DECL) {
1533+
const xmlNs *this_ns = (const xmlNs *) this;
1534+
const xmlNs *other_ns = (const xmlNs *) other;
1535+
return xmlStrEqual(this_ns->prefix, other_ns->prefix) && xmlStrEqual(this_ns->href, other_ns->href);
1536+
}
1537+
1538+
return false;
1539+
}
1540+
1541+
/* {{{ URL: https://dom.spec.whatwg.org/#dom-node-isequalnode (for everything still in the living spec)
1542+
* 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)
1543+
Since: DOM Level 3
1544+
*/
1545+
PHP_METHOD(DOMNode, isEqualNode)
1546+
{
1547+
zval *id, *node;
1548+
xmlNodePtr otherp, nodep;
1549+
dom_object *unused_intern;
1550+
1551+
id = ZEND_THIS;
1552+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "O!", &node, dom_node_class_entry) == FAILURE) {
1553+
RETURN_THROWS();
1554+
}
1555+
1556+
if (node == NULL) {
1557+
RETURN_FALSE;
1558+
}
1559+
1560+
DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, unused_intern);
1561+
DOM_GET_OBJ(otherp, node, xmlNodePtr, unused_intern);
1562+
1563+
if (nodep == otherp) {
1564+
RETURN_TRUE;
1565+
}
1566+
1567+
if (nodep->type == XML_DOCUMENT_FRAG_NODE || nodep->type == XML_HTML_DOCUMENT_NODE || nodep->type == XML_DOCUMENT_NODE) {
1568+
nodep = nodep->children;
1569+
}
1570+
1571+
if (otherp->type == XML_DOCUMENT_FRAG_NODE || otherp->type == XML_HTML_DOCUMENT_NODE || otherp->type == XML_DOCUMENT_NODE) {
1572+
otherp = otherp->children;
1573+
}
1574+
1575+
/* Empty fragments/documents only match if they're both empty */
1576+
if (UNEXPECTED(nodep == NULL || otherp == NULL)) {
1577+
RETURN_BOOL(nodep == NULL && otherp == NULL);
1578+
}
1579+
1580+
RETURN_BOOL(php_dom_node_is_equal_node(nodep, otherp));
1581+
}
1582+
/* }}} end DOMNode::isEqualNode */
1583+
14281584
/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#Node3-lookupNamespacePrefix
14291585
Since: DOM Level 3
14301586
*/

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)