1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

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.
This commit is contained in:
Niels Dossche
2023-07-12 18:07:53 +02:00
parent c97507b5c1
commit 2f318cfb06
6 changed files with 520 additions and 1 deletions

1
NEWS
View File

@@ -28,6 +28,7 @@ PHP NEWS
. Added DOMNode::isConnected and DOMNameSpaceNode::isConnected. (nielsdos)
. Added DOMNode::parentElement and DOMNameSpaceNode::parentElement.
(nielsdos)
. Added DOMNode::isEqualNode(). (nielsdos)
- FPM:
. Added warning to log when fpm socket was not registered on the expected

View File

@@ -270,6 +270,7 @@ PHP 8.3 UPGRADE NOTES
. Added DOMParentNode::replaceChildren().
. Added DOMNode::isConnected and DOMNameSpaceNode::isConnected.
. Added DOMNode::parentElement and DOMNameSpaceNode::parentElement.
. Added DOMNode::isEqualNode().
- JSON:
. Added json_validate(), which returns whether the json is valid for

View File

@@ -1471,6 +1471,155 @@ PHP_METHOD(DOMNode, isSameNode)
}
/* }}} end dom_node_is_same_node */
static bool php_dom_node_is_content_equal(const xmlNode *this, const xmlNode *other)
{
xmlChar *this_content = xmlNodeGetContent(this);
xmlChar *other_content = xmlNodeGetContent(other);
bool result = xmlStrEqual(this_content, other_content);
xmlFree(this_content);
xmlFree(other_content);
return result;
}
static bool php_dom_node_is_ns_uri_equal(const xmlNode *this, const xmlNode *other)
{
const xmlChar *this_ns = this->ns ? this->ns->href : NULL;
const xmlChar *other_ns = other->ns ? other->ns->href : NULL;
return xmlStrEqual(this_ns, other_ns);
}
static bool php_dom_node_is_ns_prefix_equal(const xmlNode *this, const xmlNode *other)
{
const xmlChar *this_ns = this->ns ? this->ns->prefix : NULL;
const xmlChar *other_ns = other->ns ? other->ns->prefix : NULL;
return xmlStrEqual(this_ns, other_ns);
}
static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other);
#define PHP_DOM_FUNC_CAT(prefix, suffix) prefix##_##suffix
/* xmlNode and xmlNs have incompatible struct layouts, i.e. the next field is in a different offset */
#define PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(type) \
static size_t PHP_DOM_FUNC_CAT(php_dom_node_count_list_size, type)(const type *node) \
{ \
size_t counter = 0; \
while (node) { \
counter++; \
node = node->next; \
} \
return counter; \
} \
static bool PHP_DOM_FUNC_CAT(php_dom_node_list_equality_check, type)(const type *list1, const type *list2) \
{ \
size_t count = PHP_DOM_FUNC_CAT(php_dom_node_count_list_size, type)(list1); \
if (count != PHP_DOM_FUNC_CAT(php_dom_node_count_list_size, type)(list2)) { \
return false; \
} \
for (size_t i = 0; i < count; i++) { \
if (!php_dom_node_is_equal_node((const xmlNode *) list1, (const xmlNode *) list2)) { \
return false; \
} \
list1 = list1->next; \
list2 = list2->next; \
} \
return true; \
}
PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(xmlNode)
PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(xmlNs)
static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other)
{
ZEND_ASSERT(this != NULL);
ZEND_ASSERT(other != NULL);
if (this->type != other->type) {
return false;
}
/* Notes:
* - XML_DOCUMENT_TYPE_NODE is no longer created by libxml2, we only have to support XML_DTD_NODE.
* - element and attribute declarations are not exposed as nodes in DOM, so no comparison is needed for those. */
if (this->type == XML_ELEMENT_NODE) {
return xmlStrEqual(this->name, other->name)
&& php_dom_node_is_ns_prefix_equal(this, other)
&& php_dom_node_is_ns_uri_equal(this, other)
/* Check attributes first, then namespace declarations, then children */
&& php_dom_node_list_equality_check_xmlNode((const xmlNode *) this->properties, (const xmlNode *) other->properties)
&& php_dom_node_list_equality_check_xmlNs(this->nsDef, other->nsDef)
&& php_dom_node_list_equality_check_xmlNode(this->children, other->children);
} else if (this->type == XML_DTD_NODE) {
/* Note: in the living spec entity declarations and notations are no longer compared because they're considered obsolete. */
const xmlDtd *this_dtd = (const xmlDtd *) this;
const xmlDtd *other_dtd = (const xmlDtd *) other;
return xmlStrEqual(this_dtd->name, other_dtd->name)
&& xmlStrEqual(this_dtd->ExternalID, other_dtd->ExternalID)
&& xmlStrEqual(this_dtd->SystemID, other_dtd->SystemID);
} else if (this->type == XML_PI_NODE) {
return xmlStrEqual(this->name, other->name) && xmlStrEqual(this->content, other->content);
} else if (this->type == XML_TEXT_NODE || this->type == XML_COMMENT_NODE || this->type == XML_CDATA_SECTION_NODE) {
return xmlStrEqual(this->content, other->content);
} else if (this->type == XML_ATTRIBUTE_NODE) {
const xmlAttr *this_attr = (const xmlAttr *) this;
const xmlAttr *other_attr = (const xmlAttr *) other;
return xmlStrEqual(this_attr->name, other_attr->name)
&& php_dom_node_is_ns_uri_equal(this, other)
&& php_dom_node_is_content_equal(this, other);
} else if (this->type == XML_ENTITY_REF_NODE) {
return xmlStrEqual(this->name, other->name);
} else if (this->type == XML_ENTITY_DECL || this->type == XML_NOTATION_NODE || this->type == XML_ENTITY_NODE) {
const xmlEntity *this_entity = (const xmlEntity *) this;
const xmlEntity *other_entity = (const xmlEntity *) other;
return this_entity->etype == other_entity->etype
&& xmlStrEqual(this_entity->name, other_entity->name)
&& xmlStrEqual(this_entity->ExternalID, other_entity->ExternalID)
&& xmlStrEqual(this_entity->SystemID, other_entity->SystemID)
&& php_dom_node_is_content_equal(this, other);
} else if (this->type == XML_NAMESPACE_DECL) {
const xmlNs *this_ns = (const xmlNs *) this;
const xmlNs *other_ns = (const xmlNs *) other;
return xmlStrEqual(this_ns->prefix, other_ns->prefix) && xmlStrEqual(this_ns->href, other_ns->href);
} else if (this->type == XML_DOCUMENT_FRAG_NODE || this->type == XML_HTML_DOCUMENT_NODE || this->type == XML_DOCUMENT_NODE) {
return php_dom_node_list_equality_check_xmlNode(this->children, other->children);
}
return false;
}
/* {{{ URL: https://dom.spec.whatwg.org/#dom-node-isequalnode (for everything still in the living spec)
* 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)
Since: DOM Level 3
*/
PHP_METHOD(DOMNode, isEqualNode)
{
zval *id, *node;
xmlNodePtr otherp, nodep;
dom_object *unused_intern;
id = ZEND_THIS;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "O!", &node, dom_node_class_entry) == FAILURE) {
RETURN_THROWS();
}
if (node == NULL) {
RETURN_FALSE;
}
DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, unused_intern);
DOM_GET_OBJ(otherp, node, xmlNodePtr, unused_intern);
if (nodep == otherp) {
RETURN_TRUE;
}
/* Empty fragments/documents only match if they're both empty */
if (UNEXPECTED(nodep == NULL || otherp == NULL)) {
RETURN_BOOL(nodep == NULL && otherp == NULL);
}
RETURN_BOOL(php_dom_node_is_equal_node(nodep, otherp));
}
/* }}} end DOMNode::isEqualNode */
/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#Node3-lookupNamespacePrefix
Since: DOM Level 3
*/

View File

@@ -381,6 +381,8 @@ class DOMNode
/** @tentative-return-type */
public function isSameNode(DOMNode $otherNode): bool {}
public function isEqualNode(?DOMNode $otherNode): bool {}
/** @tentative-return-type */
public function isSupported(string $feature, string $version): bool {}

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 02e8c582a3a7d88fc32ef8931c23c0c6de5a94e2 */
* Stub hash: 7070b07b2dee16222242b7e516372a6562d87036 */
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0)
ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0)
@@ -77,6 +77,10 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_DOMNode_isSameNo
ZEND_ARG_OBJ_INFO(0, otherNode, DOMNode, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_DOMNode_isEqualNode, 0, 1, _IS_BOOL, 0)
ZEND_ARG_OBJ_INFO(0, otherNode, DOMNode, 1)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_DOMNode_isSupported, 0, 2, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, feature, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, version, IS_STRING, 0)
@@ -525,6 +529,7 @@ ZEND_METHOD(DOMNode, hasChildNodes);
ZEND_METHOD(DOMNode, insertBefore);
ZEND_METHOD(DOMNode, isDefaultNamespace);
ZEND_METHOD(DOMNode, isSameNode);
ZEND_METHOD(DOMNode, isEqualNode);
ZEND_METHOD(DOMNode, isSupported);
ZEND_METHOD(DOMNode, lookupNamespaceURI);
ZEND_METHOD(DOMNode, lookupPrefix);
@@ -713,6 +718,7 @@ static const zend_function_entry class_DOMNode_methods[] = {
ZEND_ME(DOMNode, insertBefore, arginfo_class_DOMNode_insertBefore, ZEND_ACC_PUBLIC)
ZEND_ME(DOMNode, isDefaultNamespace, arginfo_class_DOMNode_isDefaultNamespace, ZEND_ACC_PUBLIC)
ZEND_ME(DOMNode, isSameNode, arginfo_class_DOMNode_isSameNode, ZEND_ACC_PUBLIC)
ZEND_ME(DOMNode, isEqualNode, arginfo_class_DOMNode_isEqualNode, ZEND_ACC_PUBLIC)
ZEND_ME(DOMNode, isSupported, arginfo_class_DOMNode_isSupported, ZEND_ACC_PUBLIC)
ZEND_ME(DOMNode, lookupNamespaceURI, arginfo_class_DOMNode_lookupNamespaceURI, ZEND_ACC_PUBLIC)
ZEND_ME(DOMNode, lookupPrefix, arginfo_class_DOMNode_lookupPrefix, ZEND_ACC_PUBLIC)

View File

@@ -0,0 +1,360 @@
--TEST--
DOMNode::isEqualNode()
--EXTENSIONS--
dom
--FILE--
<?php
$dom1 = new DOMDocument();
$dom2 = new DOMDocument();
$dom1->loadXML(<<<XML
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd" [
<!ENTITY bar '<bar>bartext</bar>'>
<!ENTITY foo '<foo/>'>
<!NOTATION myNotation SYSTEM "test.dtd">
]>
<html>
<body>
<pi><?test 123?><?test2 123?><?test2 1234?><?test 123?></pi>
<ps><p>text</p><p>text</p><p>other text</p></ps>
<psattrs><p align="center" xmlns:foo="some:bar">text</p><p xmlns:foo="some:bar">text</p><p align="center">text</p><p align="left" xmlns:foo="some:bar">text</p><p align="center" xmlns:foo="some:bar2">text</p><p align="center" xmlns:foo="some:bar">text</p></psattrs>
<comments><!-- comment 1 --><!-- comment 2 --><!-- comment 1 --></comments>
<texts>abc<i/>def<i/>abc</texts>
<cdatas><![CDATA[test]]> <![CDATA[test2]]> <![CDATA[test]]></cdatas>
<tree>
<div>
<p>A</p>
<div foo="bar">
<p>B</p>
</div>
</div>
<p>A</p>
</tree>
</body>
</html>
XML);
$xpath = new DOMXPath($dom1);
function foreach_comparator($query) {
global $xpath;
$container = $xpath->query($query)[0];
$childNodes = iterator_to_array($container->childNodes);
$firstChild = $childNodes[0];
foreach ($childNodes as $child) {
var_dump($child->isEqualNode($firstChild));
}
}
function comparePairs($list1, $list2) {
$list1 = iterator_to_array($list1);
$list2 = iterator_to_array($list2);
usort($list1, function ($a, $b) {
return strcmp($a->nodeName, $b->nodeName);
});
usort($list2, function ($a, $b) {
return strcmp($a->nodeName, $b->nodeName);
});
foreach ($list1 as $entity1) {
foreach ($list2 as $entity2) {
echo "Comparing {$entity1->nodeName} with {$entity2->nodeName}\n";
var_dump($entity1->isEqualNode($entity2));
}
}
}
echo "--- Test edge cases ---\n";
var_dump($dom1->doctype->isEqualNode(null));
var_dump((new DOMDocument())->isEqualNode(new DOMDocument()));
echo "--- Test doctype ---\n";
var_dump($dom1->doctype->isEqualNode($dom1->doctype));
$dom2->loadXML('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/x.dtd"><html/>');
var_dump($dom1->doctype->isEqualNode($dom2->doctype));
$dom2->loadXML('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN2" "http://www.w3.org/TR/html4/strict.dtd"><html/>');
var_dump($dom1->doctype->isEqualNode($dom2->doctype));
$dom2->loadXML('<!DOCTYPE HTML2 PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html/>');
var_dump($dom1->doctype->isEqualNode($dom2->doctype));
$dom2->loadXML('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html/>');
var_dump($dom1->doctype->isEqualNode($dom2->doctype));
echo "--- Test processing instruction ---\n";
foreach_comparator("//pi");
echo "--- Test comments ---\n";
foreach_comparator("//comments");
echo "--- Test texts ---\n";
foreach_comparator("//texts");
echo "--- Test CDATA ---\n";
foreach_comparator("//cdatas");
echo "--- Test attribute ---\n";
var_dump((new DOMAttr("name", "value"))->isEqualNode(new DOMAttr("name", "value")));
var_dump((new DOMAttr("name", "value"))->isEqualNode(new DOMAttr("name", "value2")));
var_dump((new DOMAttr("name", "value"))->isEqualNode(new DOMAttr("name2", "value")));
var_dump((new DOMAttr("name", "value"))->isEqualNode(new DOMAttr("name2", "value2")));
var_dump((new DOMAttr("name", "value"))->isEqualNode(new DOMAttr("ns:name", "value")));
echo "--- Test entity reference ---\n";
var_dump((new DOMEntityReference("ref"))->isEqualNode(new DOMEntityReference("ref")));
var_dump((new DOMEntityReference("ref"))->isEqualNode(new DOMEntityReference("ref2")));
echo "--- Test entity declaration ---\n";
$dom2->loadXML(<<<XML
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd" [
<!ENTITY barbar '<bar>bartext</bar>'>
<!ENTITY foo '<foo2/>'>
<!ENTITY bar '<bar>bartext</bar>'>
]>
<html/>
XML);
comparePairs($dom1->doctype->entities, $dom2->doctype->entities);
echo "--- Test notation declaration ---\n";
$dom2->loadXML(<<<XML
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd" [
<!NOTATION myNotation SYSTEM "test.dtd">
<!NOTATION myNotation2 SYSTEM "test2.dtd">
<!NOTATION myNotation3 SYSTEM "test.dtd">
]>
<html/>
XML);
comparePairs($dom1->doctype->notations, $dom2->doctype->notations);
echo "--- Test element without attributes ---\n";
foreach_comparator("//ps");
echo "--- Test element with attributes ---\n";
foreach_comparator("//psattrs");
echo "--- Test element tree ---\n";
$tree = $xpath->query("//tree")[0];
$dom2->loadXML(<<<XML
<tree>
<div>
<p>A</p>
<div foo="bar">
<p>B</p>
</div>
</div>
<p>A</p>
</tree>
XML);
var_dump($tree->isEqualNode($dom2->documentElement));
$dom2->loadXML(<<<XML
<tree>
<p>A</p>
<div>
<p>A</p>
<div foo="bar">
<p>B</p>
</div>
</div>
</tree>
XML);
var_dump($tree->isEqualNode($dom2->documentElement));
$dom2->loadXML(<<<XML
<tree>
<div>
<p>A</p>
<div foo="bar">
</div>
</div>
<p>A</p>
</tree>
XML);
var_dump($tree->isEqualNode($dom2->documentElement));
$dom2->loadXML(<<<XML
<tree>
<div>
<p>A</p>
<div foo="bar" extra="attr">
<p>B</p>
</div>
</div>
<p>A</p>
</tree>
XML);
var_dump($tree->isEqualNode($dom2->documentElement));
echo "--- Test documents ---\n";
$dom1Clone = clone $dom1;
var_dump($dom1->documentElement->isEqualNode($dom1Clone->documentElement));
var_dump($dom1->documentElement->isEqualNode($dom2->documentElement));
var_dump($dom1->isEqualNode($dom1Clone));
var_dump($dom1->isEqualNode($dom2));
var_dump($dom1->documentElement->isEqualNode($dom1Clone));
var_dump($dom1->documentElement->isEqualNode($dom2));
echo "--- Test document fragments ---\n";
$fragment1 = $dom1->createDocumentFragment();
$fragment1->appendChild($dom1->createElement('em'));
$fragment2 = $dom1->createDocumentFragment();
$fragment2->appendChild($dom1->createElement('em'));
$fragment3 = $dom1->createDocumentFragment();
$fragment3->appendChild($dom1->createElement('b'));
$emptyFragment1 = $dom1->createDocumentFragment();
$emptyFragment2 = $dom1->createDocumentFragment();
var_dump($fragment1->isEqualNode($fragment2));
var_dump($fragment1->isEqualNode($fragment3));
var_dump($emptyFragment1->isEqualNode($fragment1));
var_dump($emptyFragment1->isEqualNode($emptyFragment2));
echo "--- Test document fragments with multiple child nodes ---\n";
$fragment1 = $dom1->createDocumentFragment();
$fragment1->appendChild($dom1->createElement('a'));
$fragment1->appendChild($dom1->createElement('b'));
$fragment1->appendChild($dom1->createElement('c'));
$fragment2 = $dom2->createDocumentFragment();
$fragment2->appendChild($dom2->createElement('a'));
$fragment2->appendChild($dom2->createElement('b'));
$fragment2->appendChild($dom2->createElement('c'));
var_dump($fragment1->isEqualNode($fragment2));
$fragment2->firstChild->nextSibling->nextSibling->remove();
var_dump($fragment1->isEqualNode($fragment2));
echo "--- Test x:includes ---\n";
// Adapted from https://www.php.net/manual/en/domdocument.xinclude.php
$dom = new DOMDocument();
$dom->loadXML(<<<XML
<?xml version="1.0" ?>
<chapter xmlns:xi="http://www.w3.org/2001/XInclude">
<p>Hello</p>
<para>
<xi:include href="book.xml">
<xi:fallback>
<p>xinclude: book.xml not found</p>
</xi:fallback>
</xi:include>
</para>
<para><p>xinclude: book.xml not found</p></para>
</chapter>
XML);
@$dom->xinclude();
$xpath = new DOMXPath($dom);
$firstPara = $dom->documentElement->firstElementChild->nextElementSibling;
$secondPara = $dom->documentElement->firstElementChild->nextElementSibling->nextElementSibling;
var_dump($firstPara->isEqualNode($secondPara));
var_dump($firstPara->firstElementChild->isEqualNode($secondPara->firstElementChild));
?>
--EXPECT--
--- Test edge cases ---
bool(false)
bool(true)
--- Test doctype ---
bool(true)
bool(false)
bool(false)
bool(false)
bool(true)
--- Test processing instruction ---
bool(true)
bool(false)
bool(false)
bool(true)
--- Test comments ---
bool(true)
bool(false)
bool(true)
--- Test texts ---
bool(true)
bool(false)
bool(false)
bool(false)
bool(true)
--- Test CDATA ---
bool(true)
bool(false)
bool(false)
bool(false)
bool(true)
--- Test attribute ---
bool(true)
bool(false)
bool(false)
bool(false)
bool(false)
--- Test entity reference ---
bool(true)
bool(false)
--- Test entity declaration ---
Comparing bar with bar
bool(true)
Comparing bar with barbar
bool(false)
Comparing bar with foo
bool(false)
Comparing foo with bar
bool(false)
Comparing foo with barbar
bool(false)
Comparing foo with foo
bool(true)
--- Test notation declaration ---
Comparing myNotation with myNotation
bool(true)
Comparing myNotation with myNotation2
bool(false)
Comparing myNotation with myNotation3
bool(false)
--- Test element without attributes ---
bool(true)
bool(true)
bool(false)
--- Test element with attributes ---
bool(true)
bool(false)
bool(false)
bool(false)
bool(false)
bool(true)
--- Test element tree ---
bool(true)
bool(false)
bool(false)
bool(false)
--- Test documents ---
bool(true)
bool(false)
bool(true)
bool(false)
bool(false)
bool(false)
--- Test document fragments ---
bool(true)
bool(false)
bool(false)
bool(true)
--- Test document fragments with multiple child nodes ---
bool(true)
bool(false)
--- Test x:includes ---
bool(false)
bool(true)