From c2695418232982d887ebaaf48498c52864d6e14c Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:40:36 +0100 Subject: [PATCH] Implement PHP-specific extensions to Dom See RFC: https://wiki.php.net/rfc/dom_additions_84 --- ext/dom/dom_ce.h | 1 + ext/dom/dom_properties.h | 2 + ext/dom/element.c | 215 ++++++++++++++++++ ext/dom/php_dom.c | 4 + ext/dom/php_dom.stub.php | 27 +++ ext/dom/php_dom_arginfo.h | 63 ++++- .../Element_getDescendantNamespaces.phpt | 125 ++++++++++ .../Element_getInScopeNamespaces.phpt | 81 +++++++ .../Element_renaming_html_ns_01.phpt | 25 ++ .../Element_renaming_html_ns_02.phpt | 25 ++ .../Element_substitutedNodeValue.phpt | 38 ++++ .../attribute_renaming_conflict.phpt | 75 ++++++ .../modern/extensions/node_renaming.phpt | 98 ++++++++ .../node_renaming_validation_errors.phpt | 29 +++ 14 files changed, 807 insertions(+), 1 deletion(-) create mode 100644 ext/dom/tests/modern/extensions/Element_getDescendantNamespaces.phpt create mode 100644 ext/dom/tests/modern/extensions/Element_getInScopeNamespaces.phpt create mode 100644 ext/dom/tests/modern/extensions/Element_renaming_html_ns_01.phpt create mode 100644 ext/dom/tests/modern/extensions/Element_renaming_html_ns_02.phpt create mode 100644 ext/dom/tests/modern/extensions/Element_substitutedNodeValue.phpt create mode 100644 ext/dom/tests/modern/extensions/attribute_renaming_conflict.phpt create mode 100644 ext/dom/tests/modern/extensions/node_renaming.phpt create mode 100644 ext/dom/tests/modern/extensions/node_renaming_validation_errors.phpt diff --git a/ext/dom/dom_ce.h b/ext/dom/dom_ce.h index 04870ed955101..5106738a0afdb 100644 --- a/ext/dom/dom_ce.h +++ b/ext/dom/dom_ce.h @@ -69,5 +69,6 @@ extern PHP_DOM_EXPORT zend_class_entry *dom_modern_xpath_class_entry; #endif extern PHP_DOM_EXPORT zend_class_entry *dom_namespace_node_class_entry; extern PHP_DOM_EXPORT zend_class_entry *dom_adjacent_position_class_entry; +extern PHP_DOM_EXPORT zend_class_entry *dom_namespace_info_class_entry; #endif /* DOM_CE_H */ diff --git a/ext/dom/dom_properties.h b/ext/dom/dom_properties.h index 1c9d0c6e34ad4..ba6e4e6370132 100644 --- a/ext/dom/dom_properties.h +++ b/ext/dom/dom_properties.h @@ -86,6 +86,8 @@ zend_result dom_element_schema_type_info_read(dom_object *obj, zval *retval); zend_result dom_element_inner_html_read(dom_object *obj, zval *retval); zend_result dom_element_inner_html_write(dom_object *obj, zval *newval); zend_result dom_element_class_list_read(dom_object *obj, zval *retval); +zend_result dom_modern_element_substituted_node_value_read(dom_object *obj, zval *retval); +zend_result dom_modern_element_substituted_node_value_write(dom_object *obj, zval *newval); /* entity properties */ zend_result dom_entity_public_id_read(dom_object *obj, zval *retval); diff --git a/ext/dom/element.c b/ext/dom/element.c index bc375562478b2..9bd762a9b9e9d 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -1842,4 +1842,219 @@ PHP_METHOD(Dom_Element, closest) dom_element_closest(thisp, intern, return_value, selectors_str); } +zend_result dom_modern_element_substituted_node_value_read(dom_object *obj, zval *retval) +{ + DOM_PROP_NODE(xmlNodePtr, nodep, obj); + + xmlChar *content = xmlNodeGetContent(nodep); + + if (UNEXPECTED(content == NULL)) { + php_dom_throw_error(INVALID_STATE_ERR, true); + return FAILURE; + } else { + ZVAL_STRING(retval, (const char *) content); + xmlFree(content); + } + + return SUCCESS; +} + +zend_result dom_modern_element_substituted_node_value_write(dom_object *obj, zval *newval) +{ + DOM_PROP_NODE(xmlNodePtr, nodep, obj); + + php_libxml_invalidate_node_list_cache(obj->document); + dom_remove_all_children(nodep); + xmlNodeSetContentLen(nodep, (xmlChar *) Z_STRVAL_P(newval), Z_STRLEN_P(newval)); + + return SUCCESS; +} + +static void dom_element_get_in_scope_namespace_info(php_dom_libxml_ns_mapper *ns_mapper, HashTable *result, xmlNodePtr nodep, dom_object *intern) +{ + HashTable prefix_to_ns_table; + zend_hash_init(&prefix_to_ns_table, 0, NULL, NULL, false); + zend_hash_real_init_mixed(&prefix_to_ns_table); + + /* https://www.w3.org/TR/1999/REC-xpath-19991116/#namespace-nodes */ + for (const xmlNode *cur = nodep; cur != NULL; cur = cur->parent) { + if (cur->type == XML_ELEMENT_NODE) { + /* Find the last attribute */ + const xmlAttr *last = NULL; + for (const xmlAttr *attr = cur->properties; attr != NULL; attr = attr->next) { + last = attr; + } + + /* Reversed loop because the parent traversal is reversed as well, + * this will keep the ordering consistent. */ + for (const xmlAttr *attr = last; attr != NULL; attr = attr->prev) { + if (attr->ns != NULL && php_dom_ns_is_fast_ex(attr->ns, php_dom_ns_is_xmlns_magic_token) + && attr->children != NULL && attr->children->content != NULL) { + const char *prefix = attr->ns->prefix == NULL ? NULL : (const char *) attr->name; + const char *key = prefix == NULL ? "" : prefix; + xmlNsPtr ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, prefix, (const char *) attr->children->content); + /* NULL is a valid value for the sentinel */ + zval zv; + ZVAL_PTR(&zv, ns); + zend_hash_str_add(&prefix_to_ns_table, key, strlen(key), &zv); + } + } + } + } + + xmlNsPtr ns; + zend_string *prefix; + ZEND_HASH_MAP_REVERSE_FOREACH_STR_KEY_PTR(&prefix_to_ns_table, prefix, ns) { + if (ZSTR_LEN(prefix) == 0 && (ns == NULL || ns->href == NULL || *ns->href == '\0')) { + /* Exception: "the value of the xmlns attribute for the nearest such element is non-empty" */ + continue; + } + + zval zv; + object_init_ex(&zv, dom_namespace_info_class_entry); + zend_object *obj = Z_OBJ(zv); + + if (ZSTR_LEN(prefix) != 0) { + ZVAL_STR_COPY(OBJ_PROP_NUM(obj, 0), prefix); + } else { + ZVAL_NULL(OBJ_PROP_NUM(obj, 0)); + } + + if (ns != NULL && ns->href != NULL && *ns->href != '\0') { + ZVAL_STRING(OBJ_PROP_NUM(obj, 1), (const char *) ns->href); + } else { + ZVAL_NULL(OBJ_PROP_NUM(obj, 1)); + } + + php_dom_create_object(nodep, OBJ_PROP_NUM(obj, 2), intern); + + zend_hash_next_index_insert_new(result, &zv); + } ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(&prefix_to_ns_table); +} + +PHP_METHOD(Dom_Element, getInScopeNamespaces) +{ + zval *id; + xmlNode *nodep; + dom_object *intern; + + ZEND_PARSE_PARAMETERS_NONE(); + + DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, intern); + + php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(intern); + + array_init(return_value); + HashTable *result = Z_ARRVAL_P(return_value); + + dom_element_get_in_scope_namespace_info(ns_mapper, result, nodep, intern); +} + +PHP_METHOD(Dom_Element, getDescendantNamespaces) +{ + zval *id; + xmlNode *nodep; + dom_object *intern; + + ZEND_PARSE_PARAMETERS_NONE(); + + DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, intern); + + php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(intern); + + array_init(return_value); + HashTable *result = Z_ARRVAL_P(return_value); + + dom_element_get_in_scope_namespace_info(ns_mapper, result, nodep, intern); + + xmlNodePtr cur = nodep->children; + while (cur != NULL) { + if (cur->type == XML_ELEMENT_NODE) { + /* TODO: this could be more optimized by updating the same HashTable repeatedly + * instead of recreating it on every node. */ + dom_element_get_in_scope_namespace_info(ns_mapper, result, cur, intern); + } + + cur = php_dom_next_in_tree_order(cur, nodep); + } +} + +PHP_METHOD(Dom_Element, rename) +{ + zend_string *namespace_uri, *qualified_name; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR_OR_NULL(namespace_uri) + Z_PARAM_STR(qualified_name) + ZEND_PARSE_PARAMETERS_END(); + + zval *id; + dom_object *intern; + xmlNodePtr nodep; + DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, intern); + + xmlChar *localname = NULL, *prefix = NULL; + int errorcode = dom_validate_and_extract(namespace_uri, qualified_name, &localname, &prefix); + if (UNEXPECTED(errorcode != 0)) { + php_dom_throw_error(errorcode, /* strict */ true); + goto cleanup; + } + + if (nodep->type == XML_ATTRIBUTE_NODE) { + /* Check for duplicate attributes. */ + xmlAttrPtr existing = xmlHasNsProp(nodep->parent, localname, namespace_uri && ZSTR_VAL(namespace_uri)[0] != '\0' ? BAD_CAST ZSTR_VAL(namespace_uri) : NULL); + if (existing != NULL && existing != (xmlAttrPtr) nodep) { + php_dom_throw_error_with_message(INVALID_MODIFICATION_ERR, "An attribute with the given name in the given namespace already exists", /* strict */ true); + goto cleanup; + } + } else { + ZEND_ASSERT(nodep->type == XML_ELEMENT_NODE); + + /* Check for moving to or away from the HTML namespace. */ + bool is_currently_html_ns = php_dom_ns_is_fast(nodep, php_dom_ns_is_html_magic_token); + bool will_be_html_ns = namespace_uri != NULL && zend_string_equals_literal(namespace_uri, DOM_XHTML_NS_URI); + if (is_currently_html_ns != will_be_html_ns) { + if (is_currently_html_ns) { + php_dom_throw_error_with_message( + INVALID_MODIFICATION_ERR, + "It is not possible to move an element out of the HTML namespace because the HTML namespace is tied to the HTMLElement class", + /* strict */ true + ); + } else { + php_dom_throw_error_with_message( + INVALID_MODIFICATION_ERR, + "It is not possible to move an element into the HTML namespace because the HTML namespace is tied to the HTMLElement class", + /* strict */ true + ); + } + goto cleanup; + } + } + + php_libxml_invalidate_node_list_cache(intern->document); + + php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(intern); + + /* Update namespace uri + prefix by querying the namespace mapper */ + /* prefix can be NULL here, but that is taken care of by the called APIs. */ + nodep->ns = php_dom_libxml_ns_mapper_get_ns_raw_prefix_string(ns_mapper, prefix, xmlStrlen(prefix), namespace_uri); + + /* Change the local name */ + if (xmlDictOwns(nodep->doc->dict, nodep->name) != 1) { + xmlFree((xmlChar *) nodep->name); + } + const xmlChar *copy = xmlDictLookup(nodep->doc->dict, localname, -1); + if (copy != NULL) { + nodep->name = copy; + } else { + nodep->name = localname; + localname = NULL; + } + +cleanup: + xmlFree(localname); + xmlFree(prefix); +} + #endif diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 71674f9715bfb..1fdeaf514d592 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -89,6 +89,7 @@ PHP_DOM_EXPORT zend_class_entry *dom_modern_xpath_class_entry; #endif PHP_DOM_EXPORT zend_class_entry *dom_namespace_node_class_entry; PHP_DOM_EXPORT zend_class_entry *dom_adjacent_position_class_entry; +PHP_DOM_EXPORT zend_class_entry *dom_namespace_info_class_entry; /* }}} */ static zend_object_handlers dom_object_handlers; @@ -840,6 +841,8 @@ PHP_MINIT_FUNCTION(dom) DOM_REGISTER_PROP_HANDLER(&dom_namespace_node_prop_handlers, "parentElement", dom_node_parent_element_read, NULL); zend_hash_add_new_ptr(&classes, dom_namespace_node_class_entry->name, &dom_namespace_node_prop_handlers); + dom_namespace_info_class_entry = register_class_Dom_NamespaceInfo(); + dom_documentfragment_class_entry = register_class_DOMDocumentFragment(dom_node_class_entry, dom_parentnode_class_entry); dom_documentfragment_class_entry->create_object = dom_objects_new; dom_documentfragment_class_entry->default_object_handlers = &dom_object_handlers; @@ -1066,6 +1069,7 @@ PHP_MINIT_FUNCTION(dom) DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "previousElementSibling", dom_node_previous_element_sibling_read, NULL); DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "nextElementSibling", dom_node_next_element_sibling_read, NULL); DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "innerHTML", dom_element_inner_html_read, dom_element_inner_html_write); + DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "substitutedNodeValue", dom_modern_element_substituted_node_value_read, dom_modern_element_substituted_node_value_write); zend_hash_merge(&dom_modern_element_prop_handlers, &dom_modern_node_prop_handlers, NULL, false); DOM_OVERWRITE_PROP_HANDLER(&dom_modern_element_prop_handlers, "textContent", dom_node_text_content_read, dom_node_text_content_write); zend_hash_add_new_ptr(&classes, dom_modern_element_class_entry->name, &dom_modern_element_prop_handlers); diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index 162355cb232cf..15d639fb27e94 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -1384,6 +1384,16 @@ public function closest(string $selectors): ?Element {} public function matches(string $selectors): bool {} public string $innerHTML; + + public string $substitutedNodeValue; + + /** @return list */ + public function getInScopeNamespaces(): array {} + + /** @return list */ + public function getDescendantNamespaces(): array {} + + public function rename(?string $namespaceURI, string $qualifiedName): void {} } class HTMLElement extends Element @@ -1410,6 +1420,9 @@ class Attr extends Node /** @implementation-alias DOMAttr::isId */ public function isId(): bool {} + + /** @implementation-alias Dom\Element::rename */ + public function rename(?string $namespaceURI, string $qualifiedName): void {} } class CharacterData extends Node implements ChildNode @@ -1688,6 +1701,20 @@ public function count(): int {} public function getIterator(): \Iterator {} } + /** + * @not-serializable + * @strict-properties + */ + readonly final class NamespaceInfo + { + public ?string $prefix; + public ?string $namespaceURI; + public Element $element; + + /** @implementation-alias Dom\Node::__construct */ + private function __construct() {} + } + #ifdef LIBXML_XPATH_ENABLED /** @not-serializable */ final class XPath diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index 46421eb22c2e9..fe42290b5f01a 100644 --- a/ext/dom/php_dom_arginfo.h +++ b/ext/dom/php_dom_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 1faa01d0564052dda0dc41f36033a076b8b4c31c */ + * Stub hash: 1af73c3b63ebeb5e59948990892dcf6b627a1671 */ 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) @@ -854,8 +854,19 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_Element_matches, 0, 1, ZEND_ARG_TYPE_INFO(0, selectors, IS_STRING, 0) ZEND_END_ARG_INFO() +#define arginfo_class_Dom_Element_getInScopeNamespaces arginfo_class_DOMNode___sleep + +#define arginfo_class_Dom_Element_getDescendantNamespaces arginfo_class_DOMNode___sleep + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_Element_rename, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, namespaceURI, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, qualifiedName, IS_STRING, 0) +ZEND_END_ARG_INFO() + #define arginfo_class_Dom_Attr_isId arginfo_class_Dom_Node_hasChildNodes +#define arginfo_class_Dom_Attr_rename arginfo_class_Dom_Element_rename + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_CharacterData_substringData, 0, 2, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, count, IS_LONG, 0) @@ -1111,6 +1122,8 @@ ZEND_END_ARG_INFO() #define arginfo_class_Dom_TokenList_getIterator arginfo_class_DOMNodeList_getIterator +#define arginfo_class_Dom_NamespaceInfo___construct arginfo_class_DOMDocumentFragment___construct + #if defined(LIBXML_XPATH_ENABLED) ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Dom_XPath___construct, 0, 0, 1) ZEND_ARG_OBJ_INFO(0, document, Dom\\Document, 0) @@ -1334,6 +1347,9 @@ ZEND_METHOD(Dom_Element, querySelector); ZEND_METHOD(Dom_Element, querySelectorAll); ZEND_METHOD(Dom_Element, closest); ZEND_METHOD(Dom_Element, matches); +ZEND_METHOD(Dom_Element, getInScopeNamespaces); +ZEND_METHOD(Dom_Element, getDescendantNamespaces); +ZEND_METHOD(Dom_Element, rename); ZEND_METHOD(Dom_CharacterData, appendData); ZEND_METHOD(Dom_CharacterData, insertData); ZEND_METHOD(Dom_CharacterData, deleteData); @@ -1748,6 +1764,9 @@ static const zend_function_entry class_Dom_Element_methods[] = { ZEND_ME(Dom_Element, querySelectorAll, arginfo_class_Dom_Element_querySelectorAll, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Element, closest, arginfo_class_Dom_Element_closest, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Element, matches, arginfo_class_Dom_Element_matches, ZEND_ACC_PUBLIC) + ZEND_ME(Dom_Element, getInScopeNamespaces, arginfo_class_Dom_Element_getInScopeNamespaces, ZEND_ACC_PUBLIC) + ZEND_ME(Dom_Element, getDescendantNamespaces, arginfo_class_Dom_Element_getDescendantNamespaces, ZEND_ACC_PUBLIC) + ZEND_ME(Dom_Element, rename, arginfo_class_Dom_Element_rename, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -1757,6 +1776,7 @@ static const zend_function_entry class_Dom_HTMLElement_methods[] = { static const zend_function_entry class_Dom_Attr_methods[] = { ZEND_RAW_FENTRY("isId", zim_DOMAttr_isId, arginfo_class_Dom_Attr_isId, ZEND_ACC_PUBLIC, NULL, NULL) + ZEND_RAW_FENTRY("rename", zim_Dom_Element_rename, arginfo_class_Dom_Attr_rename, ZEND_ACC_PUBLIC, NULL, NULL) ZEND_FE_END }; @@ -1894,6 +1914,11 @@ static const zend_function_entry class_Dom_TokenList_methods[] = { ZEND_FE_END }; +static const zend_function_entry class_Dom_NamespaceInfo_methods[] = { + ZEND_RAW_FENTRY("__construct", zim_Dom_Node___construct, arginfo_class_Dom_NamespaceInfo___construct, ZEND_ACC_PRIVATE, NULL, NULL) + ZEND_FE_END +}; + #if defined(LIBXML_XPATH_ENABLED) static const zend_function_entry class_Dom_XPath_methods[] = { ZEND_ME(Dom_XPath, __construct, arginfo_class_Dom_XPath___construct, ZEND_ACC_PUBLIC) @@ -3189,6 +3214,12 @@ static zend_class_entry *register_class_Dom_Element(zend_class_entry *class_entr zend_declare_typed_property(class_entry, property_innerHTML_name, &property_innerHTML_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); zend_string_release(property_innerHTML_name); + zval property_substitutedNodeValue_default_value; + ZVAL_UNDEF(&property_substitutedNodeValue_default_value); + zend_string *property_substitutedNodeValue_name = zend_string_init("substitutedNodeValue", sizeof("substitutedNodeValue") - 1, 1); + zend_declare_typed_property(class_entry, property_substitutedNodeValue_name, &property_substitutedNodeValue_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_substitutedNodeValue_name); + return class_entry; } @@ -3657,6 +3688,36 @@ static zend_class_entry *register_class_Dom_TokenList(zend_class_entry *class_en return class_entry; } +static zend_class_entry *register_class_Dom_NamespaceInfo(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Dom", "NamespaceInfo", class_Dom_NamespaceInfo_methods); + class_entry = zend_register_internal_class_ex(&ce, NULL); + class_entry->ce_flags |= ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_READONLY_CLASS; + + zval property_prefix_default_value; + ZVAL_UNDEF(&property_prefix_default_value); + zend_string *property_prefix_name = zend_string_init("prefix", sizeof("prefix") - 1, 1); + zend_declare_typed_property(class_entry, property_prefix_name, &property_prefix_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); + zend_string_release(property_prefix_name); + + zval property_namespaceURI_default_value; + ZVAL_UNDEF(&property_namespaceURI_default_value); + zend_string *property_namespaceURI_name = zend_string_init("namespaceURI", sizeof("namespaceURI") - 1, 1); + zend_declare_typed_property(class_entry, property_namespaceURI_name, &property_namespaceURI_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); + zend_string_release(property_namespaceURI_name); + + zval property_element_default_value; + ZVAL_UNDEF(&property_element_default_value); + zend_string *property_element_name = zend_string_init("element", sizeof("element") - 1, 1); + zend_string *property_element_class_Dom_Element = zend_string_init("Dom\\Element", sizeof("Dom\\Element")-1, 1); + zend_declare_typed_property(class_entry, property_element_name, &property_element_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_element_class_Dom_Element, 0, 0)); + zend_string_release(property_element_name); + + return class_entry; +} + #if defined(LIBXML_XPATH_ENABLED) static zend_class_entry *register_class_Dom_XPath(void) { diff --git a/ext/dom/tests/modern/extensions/Element_getDescendantNamespaces.phpt b/ext/dom/tests/modern/extensions/Element_getDescendantNamespaces.phpt new file mode 100644 index 0000000000000..551cec23b9e2c --- /dev/null +++ b/ext/dom/tests/modern/extensions/Element_getDescendantNamespaces.phpt @@ -0,0 +1,125 @@ +--TEST-- +DOM\Element::getDescendantNamespaces() +--EXTENSIONS-- +dom +--FILE-- +getElementsByTagName($name)[0]->getDescendantNamespaces(); + foreach ($list as $entry) { + echo "prefix: "; + var_dump($entry->prefix); + echo "namespaceURI: "; + var_dump($entry->namespaceURI); + echo "element->nodeName: "; + var_dump($entry->element->nodeName); + echo "---\n"; + } +} + +$dom = DOM\XMLDocument::createFromString(<< + + + + + + + +XML); + +dump($dom, 'c:child'); +dump($dom, 'child'); +dump($dom, 'b:sibling'); +dump($dom, 'd:child'); +dump($dom, 'root'); + +?> +--EXPECT-- +=== c:child === +prefix: string(1) "c" +namespaceURI: string(5) "urn:c" +element->nodeName: string(7) "c:child" +--- + +=== child === +prefix: string(1) "c" +namespaceURI: string(5) "urn:c" +element->nodeName: string(7) "c:child" +--- + +=== b:sibling === +prefix: NULL +namespaceURI: string(5) "urn:a" +element->nodeName: string(9) "b:sibling" +--- +prefix: string(1) "b" +namespaceURI: string(5) "urn:b" +element->nodeName: string(9) "b:sibling" +--- +prefix: string(1) "d" +namespaceURI: string(5) "urn:d" +element->nodeName: string(9) "b:sibling" +--- +prefix: NULL +namespaceURI: string(5) "urn:a" +element->nodeName: string(7) "d:child" +--- +prefix: string(1) "b" +namespaceURI: string(5) "urn:b" +element->nodeName: string(7) "d:child" +--- +prefix: string(1) "d" +namespaceURI: string(6) "urn:d2" +element->nodeName: string(7) "d:child" +--- + +=== d:child === +prefix: NULL +namespaceURI: string(5) "urn:a" +element->nodeName: string(7) "d:child" +--- +prefix: string(1) "b" +namespaceURI: string(5) "urn:b" +element->nodeName: string(7) "d:child" +--- +prefix: string(1) "d" +namespaceURI: string(6) "urn:d2" +element->nodeName: string(7) "d:child" +--- + +=== root === +prefix: NULL +namespaceURI: string(5) "urn:a" +element->nodeName: string(4) "root" +--- +prefix: string(1) "c" +namespaceURI: string(5) "urn:c" +element->nodeName: string(7) "c:child" +--- +prefix: NULL +namespaceURI: string(5) "urn:a" +element->nodeName: string(9) "b:sibling" +--- +prefix: string(1) "b" +namespaceURI: string(5) "urn:b" +element->nodeName: string(9) "b:sibling" +--- +prefix: string(1) "d" +namespaceURI: string(5) "urn:d" +element->nodeName: string(9) "b:sibling" +--- +prefix: NULL +namespaceURI: string(5) "urn:a" +element->nodeName: string(7) "d:child" +--- +prefix: string(1) "b" +namespaceURI: string(5) "urn:b" +element->nodeName: string(7) "d:child" +--- +prefix: string(1) "d" +namespaceURI: string(6) "urn:d2" +element->nodeName: string(7) "d:child" +--- diff --git a/ext/dom/tests/modern/extensions/Element_getInScopeNamespaces.phpt b/ext/dom/tests/modern/extensions/Element_getInScopeNamespaces.phpt new file mode 100644 index 0000000000000..59f33c54d4d6f --- /dev/null +++ b/ext/dom/tests/modern/extensions/Element_getInScopeNamespaces.phpt @@ -0,0 +1,81 @@ +--TEST-- +Dom\Element::getInScopeNamespaces() +--EXTENSIONS-- +dom +--FILE-- +getElementsByTagName($name)[0]->getInScopeNamespaces(); + foreach ($list as $entry) { + echo "prefix: "; + var_dump($entry->prefix); + echo "namespaceURI: "; + var_dump($entry->namespaceURI); + echo "element->nodeName: "; + var_dump($entry->element->nodeName); + echo "---\n"; + } +} + +$dom = Dom\XMLDocument::createFromString(<< + + + + + + + +XML); + +dump($dom, 'c:child'); +dump($dom, 'child'); +dump($dom, 'b:sibling'); +dump($dom, 'd:child'); +dump($dom, 'root'); + +?> +--EXPECT-- +=== c:child === +prefix: string(1) "c" +namespaceURI: string(5) "urn:c" +element->nodeName: string(7) "c:child" +--- + +=== child === + +=== b:sibling === +prefix: NULL +namespaceURI: string(5) "urn:a" +element->nodeName: string(9) "b:sibling" +--- +prefix: string(1) "b" +namespaceURI: string(5) "urn:b" +element->nodeName: string(9) "b:sibling" +--- +prefix: string(1) "d" +namespaceURI: string(5) "urn:d" +element->nodeName: string(9) "b:sibling" +--- + +=== d:child === +prefix: NULL +namespaceURI: string(5) "urn:a" +element->nodeName: string(7) "d:child" +--- +prefix: string(1) "b" +namespaceURI: string(5) "urn:b" +element->nodeName: string(7) "d:child" +--- +prefix: string(1) "d" +namespaceURI: string(6) "urn:d2" +element->nodeName: string(7) "d:child" +--- + +=== root === +prefix: NULL +namespaceURI: string(5) "urn:a" +element->nodeName: string(4) "root" +--- diff --git a/ext/dom/tests/modern/extensions/Element_renaming_html_ns_01.phpt b/ext/dom/tests/modern/extensions/Element_renaming_html_ns_01.phpt new file mode 100644 index 0000000000000..7d9106d3e74a2 --- /dev/null +++ b/ext/dom/tests/modern/extensions/Element_renaming_html_ns_01.phpt @@ -0,0 +1,25 @@ +--TEST-- +Element renaming interaction with the HTML namespace 01 +--EXTENSIONS-- +dom +--FILE-- +createElementNS("http://www.w3.org/1999/xhtml", "foo:bar"); +$el->rename("http://www.w3.org/1999/xhtml", "foo:baz"); +var_dump($el->nodeName, $el->namespaceURI, $el->prefix); + +// Very subtly *not* the HTML namespace! +$el = $dom->createElementNS("http://www.w3.org/1999/xhtml/", "foo:bar"); +$el->rename("urn:x", "foo:baz"); +var_dump($el->nodeName, $el->namespaceURI, $el->prefix); + +?> +--EXPECT-- +string(7) "foo:baz" +string(28) "http://www.w3.org/1999/xhtml" +string(3) "foo" +string(7) "foo:baz" +string(5) "urn:x" +string(3) "foo" diff --git a/ext/dom/tests/modern/extensions/Element_renaming_html_ns_02.phpt b/ext/dom/tests/modern/extensions/Element_renaming_html_ns_02.phpt new file mode 100644 index 0000000000000..76bc6fbd0cf4a --- /dev/null +++ b/ext/dom/tests/modern/extensions/Element_renaming_html_ns_02.phpt @@ -0,0 +1,25 @@ +--TEST-- +Element renaming interaction with the HTML namespace 02 +--EXTENSIONS-- +dom +--FILE-- +createElementNS("http://www.w3.org/1999/xhtml", "foo:bar"); +try { + $el->rename("urn:a", "foo:baz"); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} +$el = $dom->createElementNS("urn:a", "foo:bar"); +try { + $el->rename("http://www.w3.org/1999/xhtml", "foo:baz"); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +It is not possible to move an element out of the HTML namespace because the HTML namespace is tied to the HTMLElement class +It is not possible to move an element into the HTML namespace because the HTML namespace is tied to the HTMLElement class diff --git a/ext/dom/tests/modern/extensions/Element_substitutedNodeValue.phpt b/ext/dom/tests/modern/extensions/Element_substitutedNodeValue.phpt new file mode 100644 index 0000000000000..f8f1d6d122e44 --- /dev/null +++ b/ext/dom/tests/modern/extensions/Element_substitutedNodeValue.phpt @@ -0,0 +1,38 @@ +--TEST-- +Element::$substitutedNodeValue +--EXTENSIONS-- +dom +--FILE-- +'); + +$dom->documentElement->substitutedNodeValue = "1"; +var_dump($dom->documentElement->substitutedNodeValue); +var_dump($dom->documentElement->nodeValue); // Should always be NULL for elements +echo $dom->saveXML(), "\n"; + +$dom->documentElement->substitutedNodeValue = "<>"; +var_dump($dom->documentElement->substitutedNodeValue); +var_dump($dom->documentElement->nodeValue); // Should always be NULL for elements +echo $dom->saveXML(), "\n"; + +$dom->documentElement->substitutedNodeValue = ""; +var_dump($dom->documentElement->substitutedNodeValue); +var_dump($dom->documentElement->nodeValue); // Should always be NULL for elements +echo $dom->saveXML(), "\n"; + +?> +--EXPECT-- +string(1) "1" +NULL + +1 +string(2) "<>" +NULL + +<> +string(0) "" +NULL + + diff --git a/ext/dom/tests/modern/extensions/attribute_renaming_conflict.phpt b/ext/dom/tests/modern/extensions/attribute_renaming_conflict.phpt new file mode 100644 index 0000000000000..a63437ebd80dd --- /dev/null +++ b/ext/dom/tests/modern/extensions/attribute_renaming_conflict.phpt @@ -0,0 +1,75 @@ +--TEST-- +Renaming an attribute node to a name that already exists +--EXTENSIONS-- +dom +--FILE-- + + +]> + + + + +XML, LIBXML_DTDATTR); + +$root = $dom->documentElement; +try { + $root->attributes[0]->rename(NULL, 'c'); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} +try { + $root->attributes[0]->rename(NULL, 'c'); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} +try { + $root->attributes[1]->rename(NULL, 'a'); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} +try { + $root->attributes[1]->rename('urn:a', 'foo'); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} +try { + $root->attributes[3]->rename('', 'a'); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} +try { + $root->firstElementChild->attributes[0]->rename(NULL, 'hello'); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} +try { + $root->firstElementChild->attributes[1]->rename(NULL, 'my-attr'); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +// This is here to validate that nothing actually changed +echo $dom->saveXML(); + +?> +--EXPECT-- +An attribute with the given name in the given namespace already exists +An attribute with the given name in the given namespace already exists +An attribute with the given name in the given namespace already exists +An attribute with the given name in the given namespace already exists +An attribute with the given name in the given namespace already exists +An attribute with the given name in the given namespace already exists + + + +]> + + + + diff --git a/ext/dom/tests/modern/extensions/node_renaming.phpt b/ext/dom/tests/modern/extensions/node_renaming.phpt new file mode 100644 index 0000000000000..affb7ed7ba63c --- /dev/null +++ b/ext/dom/tests/modern/extensions/node_renaming.phpt @@ -0,0 +1,98 @@ +--TEST-- +Node renaming +--EXTENSIONS-- +dom +--FILE-- + + + +XML); + +function test($target, $namespaceURI, $qualifiedName) { + $namespaceURIPretty = json_encode($namespaceURI); + $qualifiedNamePretty = json_encode($qualifiedName); + echo "--- rename to $namespaceURIPretty $qualifiedNamePretty ---\n"; + $target->rename($namespaceURI, $qualifiedName); + echo $target->ownerDocument->saveXML(), "\n"; + var_dump($target->namespaceURI, $target->prefix); +} + +echo "=== Element test ===\n"; + +test($dom->documentElement, "urn:x", "x:foo"); +test($dom->documentElement, "urn:x", "a:foo"); +test($dom->documentElement, "", "foo"); +test($dom->documentElement, null, "bar"); + +echo "=== Attribute test ===\n"; + +$attribute = $dom->documentElement->firstElementChild->attributes[0]; + +test($attribute, "urn:x", "x:foo"); +test($attribute, "urn:x", "a:foo"); +test($attribute, "", "foo"); +test($attribute, null, "bar"); + +?> +--EXPECT-- +=== Element test === +--- rename to "urn:x" "x:foo" --- + + + + +string(5) "urn:x" +string(1) "x" +--- rename to "urn:x" "a:foo" --- + + + + +string(5) "urn:x" +string(1) "a" +--- rename to "" "foo" --- + + + + +NULL +NULL +--- rename to null "bar" --- + + + + +NULL +NULL +=== Attribute test === +--- rename to "urn:x" "x:foo" --- + + + + +string(5) "urn:x" +string(1) "x" +--- rename to "urn:x" "a:foo" --- + + + + +string(5) "urn:x" +string(1) "a" +--- rename to "" "foo" --- + + + + +NULL +NULL +--- rename to null "bar" --- + + + + +NULL +NULL diff --git a/ext/dom/tests/modern/extensions/node_renaming_validation_errors.phpt b/ext/dom/tests/modern/extensions/node_renaming_validation_errors.phpt new file mode 100644 index 0000000000000..5f899c8e814d2 --- /dev/null +++ b/ext/dom/tests/modern/extensions/node_renaming_validation_errors.phpt @@ -0,0 +1,29 @@ +--TEST-- +Node renaming validation errors +--EXTENSIONS-- +dom +--FILE-- +'); + +try { + $dom->documentElement->rename('', 'a:b'); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +try { + $dom->documentElement->rename('urn:a', 'a:b:c'); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +echo $dom->saveXML(); + +?> +--EXPECT-- +Namespace Error +Invalid Character Error + +