Skip to content

Commit c7b4b64

Browse files
committed
Implement PHP-specific extensions to Dom
See RFC: https://wiki.php.net/rfc/dom_additions_84
1 parent 042ae15 commit c7b4b64

14 files changed

+807
-1
lines changed

ext/dom/dom_ce.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,6 @@ extern PHP_DOM_EXPORT zend_class_entry *dom_modern_xpath_class_entry;
6868
#endif
6969
extern PHP_DOM_EXPORT zend_class_entry *dom_namespace_node_class_entry;
7070
extern PHP_DOM_EXPORT zend_class_entry *dom_adjacent_position_class_entry;
71+
extern PHP_DOM_EXPORT zend_class_entry *dom_namespace_info_class_entry;
7172

7273
#endif /* DOM_CE_H */

ext/dom/dom_properties.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ zend_result dom_element_class_name_write(dom_object *obj, zval *newval);
8383
zend_result dom_element_id_read(dom_object *obj, zval *retval);
8484
zend_result dom_element_id_write(dom_object *obj, zval *newval);
8585
zend_result dom_element_schema_type_info_read(dom_object *obj, zval *retval);
86+
zend_result dom_modern_element_substituted_node_value_read(dom_object *obj, zval *retval);
87+
zend_result dom_modern_element_substituted_node_value_write(dom_object *obj, zval *newval);
8688

8789
/* entity properties */
8890
zend_result dom_entity_public_id_read(dom_object *obj, zval *retval);

ext/dom/element.c

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,4 +1814,219 @@ PHP_METHOD(Dom_Element, closest)
18141814
dom_element_closest(thisp, intern, return_value, selectors_str);
18151815
}
18161816

1817+
zend_result dom_modern_element_substituted_node_value_read(dom_object *obj, zval *retval)
1818+
{
1819+
DOM_PROP_NODE(xmlNodePtr, nodep, obj);
1820+
1821+
xmlChar *content = xmlNodeGetContent(nodep);
1822+
1823+
if (UNEXPECTED(content == NULL)) {
1824+
php_dom_throw_error(INVALID_STATE_ERR, true);
1825+
return FAILURE;
1826+
} else {
1827+
ZVAL_STRING(retval, (const char *) content);
1828+
xmlFree(content);
1829+
}
1830+
1831+
return SUCCESS;
1832+
}
1833+
1834+
zend_result dom_modern_element_substituted_node_value_write(dom_object *obj, zval *newval)
1835+
{
1836+
DOM_PROP_NODE(xmlNodePtr, nodep, obj);
1837+
1838+
php_libxml_invalidate_node_list_cache(obj->document);
1839+
dom_remove_all_children(nodep);
1840+
xmlNodeSetContentLen(nodep, (xmlChar *) Z_STRVAL_P(newval), Z_STRLEN_P(newval));
1841+
1842+
return SUCCESS;
1843+
}
1844+
1845+
static void dom_element_get_in_scope_namespace_info(php_dom_libxml_ns_mapper *ns_mapper, HashTable *result, xmlNodePtr nodep, dom_object *intern)
1846+
{
1847+
HashTable prefix_to_ns_table;
1848+
zend_hash_init(&prefix_to_ns_table, 0, NULL, NULL, false);
1849+
zend_hash_real_init_mixed(&prefix_to_ns_table);
1850+
1851+
/* https://www.w3.org/TR/1999/REC-xpath-19991116/#namespace-nodes */
1852+
for (const xmlNode *cur = nodep; cur != NULL; cur = cur->parent) {
1853+
if (cur->type == XML_ELEMENT_NODE) {
1854+
/* Find the last attribute */
1855+
const xmlAttr *last = NULL;
1856+
for (const xmlAttr *attr = cur->properties; attr != NULL; attr = attr->next) {
1857+
last = attr;
1858+
}
1859+
1860+
/* Reversed loop because the parent traversal is reversed as well,
1861+
* this will keep the ordering consistent. */
1862+
for (const xmlAttr *attr = last; attr != NULL; attr = attr->prev) {
1863+
if (attr->ns != NULL && php_dom_ns_is_fast_ex(attr->ns, php_dom_ns_is_xmlns_magic_token)
1864+
&& attr->children != NULL && attr->children->content != NULL) {
1865+
const char *prefix = attr->ns->prefix == NULL ? NULL : (const char *) attr->name;
1866+
const char *key = prefix == NULL ? "" : prefix;
1867+
xmlNsPtr ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, prefix, (const char *) attr->children->content);
1868+
/* NULL is a valid value for the sentinel */
1869+
zval zv;
1870+
ZVAL_PTR(&zv, ns);
1871+
zend_hash_str_add(&prefix_to_ns_table, key, strlen(key), &zv);
1872+
}
1873+
}
1874+
}
1875+
}
1876+
1877+
xmlNsPtr ns;
1878+
zend_string *prefix;
1879+
ZEND_HASH_MAP_REVERSE_FOREACH_STR_KEY_PTR(&prefix_to_ns_table, prefix, ns) {
1880+
if (ZSTR_LEN(prefix) == 0 && (ns == NULL || ns->href == NULL || *ns->href == '\0')) {
1881+
/* Exception: "the value of the xmlns attribute for the nearest such element is non-empty" */
1882+
continue;
1883+
}
1884+
1885+
zval zv;
1886+
object_init_ex(&zv, dom_namespace_info_class_entry);
1887+
zend_object *obj = Z_OBJ(zv);
1888+
1889+
if (ZSTR_LEN(prefix) != 0) {
1890+
ZVAL_STR_COPY(OBJ_PROP_NUM(obj, 0), prefix);
1891+
} else {
1892+
ZVAL_NULL(OBJ_PROP_NUM(obj, 0));
1893+
}
1894+
1895+
if (ns != NULL && ns->href != NULL && *ns->href != '\0') {
1896+
ZVAL_STRING(OBJ_PROP_NUM(obj, 1), (const char *) ns->href);
1897+
} else {
1898+
ZVAL_NULL(OBJ_PROP_NUM(obj, 1));
1899+
}
1900+
1901+
php_dom_create_object(nodep, OBJ_PROP_NUM(obj, 2), intern);
1902+
1903+
zend_hash_next_index_insert_new(result, &zv);
1904+
} ZEND_HASH_FOREACH_END();
1905+
1906+
zend_hash_destroy(&prefix_to_ns_table);
1907+
}
1908+
1909+
PHP_METHOD(Dom_Element, getInScopeNamespaces)
1910+
{
1911+
zval *id;
1912+
xmlNode *nodep;
1913+
dom_object *intern;
1914+
1915+
ZEND_PARSE_PARAMETERS_NONE();
1916+
1917+
DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, intern);
1918+
1919+
php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(intern);
1920+
1921+
array_init(return_value);
1922+
HashTable *result = Z_ARRVAL_P(return_value);
1923+
1924+
dom_element_get_in_scope_namespace_info(ns_mapper, result, nodep, intern);
1925+
}
1926+
1927+
PHP_METHOD(Dom_Element, getDescendantNamespaces)
1928+
{
1929+
zval *id;
1930+
xmlNode *nodep;
1931+
dom_object *intern;
1932+
1933+
ZEND_PARSE_PARAMETERS_NONE();
1934+
1935+
DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, intern);
1936+
1937+
php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(intern);
1938+
1939+
array_init(return_value);
1940+
HashTable *result = Z_ARRVAL_P(return_value);
1941+
1942+
dom_element_get_in_scope_namespace_info(ns_mapper, result, nodep, intern);
1943+
1944+
xmlNodePtr cur = nodep->children;
1945+
while (cur != NULL) {
1946+
if (cur->type == XML_ELEMENT_NODE) {
1947+
/* TODO: this could be more optimized by updating the same HashTable repeatedly
1948+
* instead of recreating it on every node. */
1949+
dom_element_get_in_scope_namespace_info(ns_mapper, result, cur, intern);
1950+
}
1951+
1952+
cur = php_dom_next_in_tree_order(cur, nodep);
1953+
}
1954+
}
1955+
1956+
PHP_METHOD(Dom_Element, rename)
1957+
{
1958+
zend_string *namespace_uri, *qualified_name;
1959+
ZEND_PARSE_PARAMETERS_START(2, 2)
1960+
Z_PARAM_STR_OR_NULL(namespace_uri)
1961+
Z_PARAM_STR(qualified_name)
1962+
ZEND_PARSE_PARAMETERS_END();
1963+
1964+
zval *id;
1965+
dom_object *intern;
1966+
xmlNodePtr nodep;
1967+
DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, intern);
1968+
1969+
xmlChar *localname = NULL, *prefix = NULL;
1970+
int errorcode = dom_validate_and_extract(namespace_uri, qualified_name, &localname, &prefix);
1971+
if (UNEXPECTED(errorcode != 0)) {
1972+
php_dom_throw_error(errorcode, /* strict */ true);
1973+
goto cleanup;
1974+
}
1975+
1976+
if (nodep->type == XML_ATTRIBUTE_NODE) {
1977+
/* Check for duplicate attributes. */
1978+
xmlAttrPtr existing = xmlHasNsProp(nodep->parent, localname, namespace_uri && ZSTR_VAL(namespace_uri)[0] != '\0' ? BAD_CAST ZSTR_VAL(namespace_uri) : NULL);
1979+
if (existing != NULL && existing != (xmlAttrPtr) nodep) {
1980+
php_dom_throw_error_with_message(INVALID_MODIFICATION_ERR, "An attribute with the given name in the given namespace already exists", /* strict */ true);
1981+
goto cleanup;
1982+
}
1983+
} else {
1984+
ZEND_ASSERT(nodep->type == XML_ELEMENT_NODE);
1985+
1986+
/* Check for moving to or away from the HTML namespace. */
1987+
bool is_currently_html_ns = php_dom_ns_is_fast(nodep, php_dom_ns_is_html_magic_token);
1988+
bool will_be_html_ns = namespace_uri != NULL && zend_string_equals_literal(namespace_uri, DOM_XHTML_NS_URI);
1989+
if (is_currently_html_ns != will_be_html_ns) {
1990+
if (is_currently_html_ns) {
1991+
php_dom_throw_error_with_message(
1992+
INVALID_MODIFICATION_ERR,
1993+
"It is not possible to move an element out of the HTML namespace because the HTML namespace is tied to the HTMLElement class",
1994+
/* strict */ true
1995+
);
1996+
} else {
1997+
php_dom_throw_error_with_message(
1998+
INVALID_MODIFICATION_ERR,
1999+
"It is not possible to move an element into the HTML namespace because the HTML namespace is tied to the HTMLElement class",
2000+
/* strict */ true
2001+
);
2002+
}
2003+
goto cleanup;
2004+
}
2005+
}
2006+
2007+
php_libxml_invalidate_node_list_cache(intern->document);
2008+
2009+
php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(intern);
2010+
2011+
/* Update namespace uri + prefix by querying the namespace mapper */
2012+
/* prefix can be NULL here, but that is taken care of by the called APIs. */
2013+
nodep->ns = php_dom_libxml_ns_mapper_get_ns_raw_prefix_string(ns_mapper, prefix, xmlStrlen(prefix), namespace_uri);
2014+
2015+
/* Change the local name */
2016+
if (xmlDictOwns(nodep->doc->dict, nodep->name) != 1) {
2017+
xmlFree((xmlChar *) nodep->name);
2018+
}
2019+
const xmlChar *copy = xmlDictLookup(nodep->doc->dict, localname, -1);
2020+
if (copy != NULL) {
2021+
nodep->name = copy;
2022+
} else {
2023+
nodep->name = localname;
2024+
localname = NULL;
2025+
}
2026+
2027+
cleanup:
2028+
xmlFree(localname);
2029+
xmlFree(prefix);
2030+
}
2031+
18172032
#endif

ext/dom/php_dom.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ PHP_DOM_EXPORT zend_class_entry *dom_modern_xpath_class_entry;
8787
#endif
8888
PHP_DOM_EXPORT zend_class_entry *dom_namespace_node_class_entry;
8989
PHP_DOM_EXPORT zend_class_entry *dom_adjacent_position_class_entry;
90+
PHP_DOM_EXPORT zend_class_entry *dom_namespace_info_class_entry;
9091
/* }}} */
9192

9293
static zend_object_handlers dom_object_handlers;
@@ -815,6 +816,8 @@ PHP_MINIT_FUNCTION(dom)
815816
DOM_REGISTER_PROP_HANDLER(&dom_namespace_node_prop_handlers, "parentElement", dom_node_parent_element_read, NULL);
816817
zend_hash_add_new_ptr(&classes, dom_namespace_node_class_entry->name, &dom_namespace_node_prop_handlers);
817818

819+
dom_namespace_info_class_entry = register_class_Dom_NamespaceInfo();
820+
818821
dom_documentfragment_class_entry = register_class_DOMDocumentFragment(dom_node_class_entry, dom_parentnode_class_entry);
819822
dom_documentfragment_class_entry->create_object = dom_objects_new;
820823
dom_documentfragment_class_entry->default_object_handlers = &dom_object_handlers;
@@ -1039,6 +1042,7 @@ PHP_MINIT_FUNCTION(dom)
10391042
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "childElementCount", dom_parent_node_child_element_count, NULL);
10401043
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "previousElementSibling", dom_node_previous_element_sibling_read, NULL);
10411044
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "nextElementSibling", dom_node_next_element_sibling_read, NULL);
1045+
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "substitutedNodeValue", dom_modern_element_substituted_node_value_read, dom_modern_element_substituted_node_value_write);
10421046
zend_hash_merge(&dom_modern_element_prop_handlers, &dom_modern_node_prop_handlers, NULL, false);
10431047
DOM_OVERWRITE_PROP_HANDLER(&dom_modern_element_prop_handlers, "textContent", dom_node_text_content_read, dom_node_text_content_write);
10441048
zend_hash_add_new_ptr(&classes, dom_modern_element_class_entry->name, &dom_modern_element_prop_handlers);

ext/dom/php_dom.stub.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,16 @@ public function querySelector(string $selectors): ?Element {}
13801380
public function querySelectorAll(string $selectors): NodeList {}
13811381
public function closest(string $selectors): ?Element {}
13821382
public function matches(string $selectors): bool {}
1383+
1384+
public string $substitutedNodeValue;
1385+
1386+
/** @return list<NamespaceInfo> */
1387+
public function getInScopeNamespaces(): array {}
1388+
1389+
/** @return list<NamespaceInfo> */
1390+
public function getDescendantNamespaces(): array {}
1391+
1392+
public function rename(?string $namespaceURI, string $qualifiedName): void {}
13831393
}
13841394

13851395
class HTMLElement extends Element
@@ -1406,6 +1416,9 @@ class Attr extends Node
14061416

14071417
/** @implementation-alias DOMAttr::isId */
14081418
public function isId(): bool {}
1419+
1420+
/** @implementation-alias Dom\Element::rename */
1421+
public function rename(?string $namespaceURI, string $qualifiedName): void {}
14091422
}
14101423

14111424
class CharacterData extends Node implements ChildNode
@@ -1659,6 +1672,20 @@ public function saveXml(?Node $node = null, int $options = 0): string|false {}
16591672
public function saveXmlFile(string $filename, int $options = 0): int|false {}
16601673
}
16611674

1675+
/**
1676+
* @not-serializable
1677+
* @strict-properties
1678+
*/
1679+
final class NamespaceInfo
1680+
{
1681+
public readonly ?string $prefix;
1682+
public readonly ?string $namespaceURI;
1683+
public readonly Element $element;
1684+
1685+
/** @implementation-alias Dom\Node::__construct */
1686+
private function __construct() {}
1687+
}
1688+
16621689
#ifdef LIBXML_XPATH_ENABLED
16631690
/** @not-serializable */
16641691
final class XPath

0 commit comments

Comments
 (0)