Skip to content

Commit bb20b5f

Browse files
committed
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4: ext/dom: resolve in-scope prefixed QName values during document validation.
2 parents 534d28a + 3b5eeb9 commit bb20b5f

5 files changed

Lines changed: 238 additions & 142 deletions

File tree

ext/dom/document.c

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,35 @@ static int dom_perform_xinclude(xmlDocPtr docp, dom_object *intern, zend_long fl
17531753
return err;
17541754
}
17551755

1756+
/* For modern DOM, namespace declarations are stored as attributes (node->nsDef
1757+
* is NULL), so libxml's native validators can't resolve prefixed QNames found in
1758+
* content (e.g. an xs:QName attribute value). Temporarily relink them, mirroring
1759+
* what C14N does in dom_canonicalization(). */
1760+
typedef struct {
1761+
HashTable links;
1762+
bool active;
1763+
} dom_validate_ns_guard;
1764+
1765+
static void dom_validate_ns_guard_begin(dom_validate_ns_guard *guard, xmlDocPtr docp)
1766+
{
1767+
guard->active = php_dom_follow_spec_node((const xmlNode *) docp);
1768+
if (guard->active) {
1769+
zend_hash_init(&guard->links, 0, NULL, NULL, false);
1770+
xmlNodePtr root_element = xmlDocGetRootElement(docp);
1771+
if (root_element) {
1772+
dom_relink_ns_decls(&guard->links, root_element);
1773+
}
1774+
}
1775+
}
1776+
1777+
static void dom_validate_ns_guard_end(dom_validate_ns_guard *guard)
1778+
{
1779+
if (guard->active) {
1780+
dom_unlink_ns_decls(&guard->links);
1781+
zend_hash_destroy(&guard->links);
1782+
}
1783+
}
1784+
17561785
/* {{{ Substitutues xincludes in a DomDocument */
17571786
PHP_METHOD(DOMDocument, xinclude)
17581787
{
@@ -1828,8 +1857,11 @@ PHP_METHOD(DOMDocument, validate)
18281857
cvp->userData = NULL;
18291858
cvp->error = (xmlValidityErrorFunc) php_libxml_error_handler;
18301859
cvp->warning = (xmlValidityErrorFunc) php_libxml_error_handler;
1831-
1832-
if (xmlValidateDocument(cvp, docp)) {
1860+
dom_validate_ns_guard guard;
1861+
dom_validate_ns_guard_begin(&guard, docp);
1862+
int dtd_valid = xmlValidateDocument(cvp, docp);
1863+
dom_validate_ns_guard_end(&guard);
1864+
if (dtd_valid) {
18331865
RETVAL_TRUE;
18341866
} else {
18351867
RETVAL_FALSE;
@@ -1926,7 +1958,10 @@ static void dom_document_schema_validate(INTERNAL_FUNCTION_PARAMETERS, int type)
19261958
PHP_LIBXML_SANITIZE_GLOBALS(validate);
19271959
xmlSchemaSetValidOptions(vptr, valid_opts);
19281960
xmlSchemaSetValidErrors(vptr, php_libxml_error_handler, php_libxml_error_handler, vptr);
1961+
dom_validate_ns_guard guard;
1962+
dom_validate_ns_guard_begin(&guard, docp);
19291963
is_valid = xmlSchemaValidateDoc(vptr, docp);
1964+
dom_validate_ns_guard_end(&guard);
19301965
xmlSchemaFree(sptr);
19311966
xmlSchemaFreeValidCtxt(vptr);
19321967
PHP_LIBXML_RESTORE_GLOBALS(validate);
@@ -2024,7 +2059,10 @@ static void dom_document_relaxNG_validate(INTERNAL_FUNCTION_PARAMETERS, int type
20242059
}
20252060

20262061
xmlRelaxNGSetValidErrors(vptr, php_libxml_error_handler, php_libxml_error_handler, vptr);
2062+
dom_validate_ns_guard guard;
2063+
dom_validate_ns_guard_begin(&guard, docp);
20272064
is_valid = xmlRelaxNGValidateDoc(vptr, docp);
2065+
dom_validate_ns_guard_end(&guard);
20282066
xmlRelaxNGFree(sptr);
20292067
xmlRelaxNGFreeValidCtxt(vptr);
20302068

ext/dom/namespace_compat.c

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,4 +494,142 @@ PHP_DOM_EXPORT void php_dom_in_scope_ns_destroy(php_dom_in_scope_ns *in_scope_ns
494494
}
495495
}
496496

497+
static xmlNsPtr dom_alloc_ns_decl(HashTable *links, xmlNodePtr node)
498+
{
499+
xmlNsPtr ns = xmlMalloc(sizeof(*ns));
500+
if (!ns) {
501+
return NULL;
502+
}
503+
504+
zval *zv = zend_hash_index_lookup(links, (zend_ulong) node);
505+
if (Z_ISNULL_P(zv)) {
506+
ZVAL_LONG(zv, 1);
507+
} else {
508+
Z_LVAL_P(zv)++;
509+
}
510+
511+
memset(ns, 0, sizeof(*ns));
512+
ns->type = XML_LOCAL_NAMESPACE;
513+
ns->next = node->nsDef;
514+
node->nsDef = ns;
515+
516+
return ns;
517+
}
518+
519+
/* Mint a temporary nsDef entry so C14N finds namespaces that live on node->ns
520+
* but have no matching xmlns attribute (typical for createElementNS). */
521+
static void dom_add_synthetic_ns_decl(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
522+
{
523+
xmlNsPtr ns = dom_alloc_ns_decl(links, node);
524+
if (!ns) {
525+
return;
526+
}
527+
528+
ns->href = xmlStrdup(src_ns->href);
529+
ns->prefix = src_ns->prefix ? xmlStrdup(src_ns->prefix) : NULL;
530+
}
531+
532+
/* Same, but for attribute namespaces, which may collide by prefix with the
533+
* element's own ns or with a sibling attribute's ns. */
534+
static void dom_add_synthetic_ns_decl_for_attr(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
535+
{
536+
for (xmlNsPtr existing = node->nsDef; existing; existing = existing->next) {
537+
if (xmlStrEqual(existing->prefix, src_ns->prefix)) {
538+
return;
539+
}
540+
}
541+
542+
dom_add_synthetic_ns_decl(links, node, src_ns);
543+
}
544+
545+
static void dom_relink_ns_decls_element(HashTable *links, xmlNodePtr node)
546+
{
547+
if (node->type == XML_ELEMENT_NODE) {
548+
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
549+
if (php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
550+
xmlNsPtr ns = dom_alloc_ns_decl(links, node);
551+
if (!ns) {
552+
return;
553+
}
554+
555+
bool should_free;
556+
xmlChar *attr_value = php_libxml_attr_value(attr, &should_free);
557+
558+
ns->href = should_free ? attr_value : xmlStrdup(attr_value);
559+
ns->prefix = attr->ns->prefix ? xmlStrdup(attr->name) : NULL;
560+
ns->_private = attr;
561+
if (attr->prev) {
562+
attr->prev->next = attr->next;
563+
} else {
564+
node->properties = attr->next;
565+
}
566+
if (attr->next) {
567+
attr->next->prev = attr->prev;
568+
}
569+
}
570+
}
571+
572+
/* The default namespace is handled separately from the other namespaces in C14N.
573+
* The default namespace is explicitly looked up while the other namespaces are
574+
* deduplicated and compared to a list of visible namespaces. */
575+
if (node->ns && !node->ns->prefix) {
576+
/* Workaround for the behaviour where the xmlSearchNs() call inside c14n.c
577+
* can return the current namespace. */
578+
zend_hash_index_add_new_ptr(links, (zend_ulong) node | 1, node->ns);
579+
node->ns = xmlSearchNs(node->doc, node, NULL);
580+
} else if (node->ns) {
581+
dom_add_synthetic_ns_decl(links, node, node->ns);
582+
}
583+
584+
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
585+
if (attr->ns && !php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
586+
dom_add_synthetic_ns_decl_for_attr(links, node, attr->ns);
587+
}
588+
}
589+
}
590+
}
591+
592+
void dom_relink_ns_decls(HashTable *links, xmlNodePtr root)
593+
{
594+
dom_relink_ns_decls_element(links, root);
595+
596+
xmlNodePtr base = root;
597+
xmlNodePtr node = base->children;
598+
while (node != NULL) {
599+
dom_relink_ns_decls_element(links, node);
600+
node = php_dom_next_in_tree_order(node, base);
601+
}
602+
}
603+
604+
void dom_unlink_ns_decls(HashTable *links)
605+
{
606+
ZEND_HASH_MAP_FOREACH_NUM_KEY_VAL(links, zend_ulong h, zval *data) {
607+
if (h & 1) {
608+
xmlNodePtr node = (xmlNodePtr) (h ^ 1);
609+
node->ns = Z_PTR_P(data);
610+
} else {
611+
xmlNodePtr node = (xmlNodePtr) h;
612+
while (Z_LVAL_P(data)-- > 0) {
613+
xmlNsPtr ns = node->nsDef;
614+
node->nsDef = ns->next;
615+
616+
xmlAttrPtr attr = ns->_private;
617+
if (attr) {
618+
if (attr->prev) {
619+
attr->prev->next = attr;
620+
} else {
621+
node->properties = attr;
622+
}
623+
if (attr->next) {
624+
attr->next->prev = attr;
625+
}
626+
}
627+
628+
xmlFreeNs(ns);
629+
}
630+
}
631+
} ZEND_HASH_FOREACH_END();
632+
}
633+
634+
497635
#endif /* HAVE_LIBXML && HAVE_DOM */

ext/dom/node.c

Lines changed: 0 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,146 +2103,6 @@ PHP_METHOD(DOMNode, lookupNamespaceURI)
21032103
}
21042104
/* }}} end dom_node_lookup_namespace_uri */
21052105

2106-
/* Allocate, track and prepend a temporary nsDef entry for C14N.
2107-
* Returns the new xmlNsPtr for the caller to fill in href/prefix/_private,
2108-
* or NULL on allocation failure. */
2109-
static xmlNsPtr dom_alloc_ns_decl(HashTable *links, xmlNodePtr node)
2110-
{
2111-
xmlNsPtr ns = xmlMalloc(sizeof(*ns));
2112-
if (!ns) {
2113-
return NULL;
2114-
}
2115-
2116-
zval *zv = zend_hash_index_lookup(links, (zend_ulong) node);
2117-
if (Z_ISNULL_P(zv)) {
2118-
ZVAL_LONG(zv, 1);
2119-
} else {
2120-
Z_LVAL_P(zv)++;
2121-
}
2122-
2123-
memset(ns, 0, sizeof(*ns));
2124-
ns->type = XML_LOCAL_NAMESPACE;
2125-
ns->next = node->nsDef;
2126-
node->nsDef = ns;
2127-
2128-
return ns;
2129-
}
2130-
2131-
/* Mint a temporary nsDef entry so C14N finds namespaces that live on node->ns
2132-
* but have no matching xmlns attribute (typical for createElementNS). */
2133-
static void dom_add_synthetic_ns_decl(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
2134-
{
2135-
xmlNsPtr ns = dom_alloc_ns_decl(links, node);
2136-
if (!ns) {
2137-
return;
2138-
}
2139-
2140-
ns->href = xmlStrdup(src_ns->href);
2141-
ns->prefix = src_ns->prefix ? xmlStrdup(src_ns->prefix) : NULL;
2142-
}
2143-
2144-
/* Same, but for attribute namespaces, which may collide by prefix with the
2145-
* element's own ns or with a sibling attribute's ns. */
2146-
static void dom_add_synthetic_ns_decl_for_attr(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
2147-
{
2148-
for (xmlNsPtr existing = node->nsDef; existing; existing = existing->next) {
2149-
if (xmlStrEqual(existing->prefix, src_ns->prefix)) {
2150-
return;
2151-
}
2152-
}
2153-
2154-
dom_add_synthetic_ns_decl(links, node, src_ns);
2155-
}
2156-
2157-
static void dom_relink_ns_decls_element(HashTable *links, xmlNodePtr node)
2158-
{
2159-
if (node->type == XML_ELEMENT_NODE) {
2160-
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
2161-
if (php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
2162-
xmlNsPtr ns = dom_alloc_ns_decl(links, node);
2163-
if (!ns) {
2164-
return;
2165-
}
2166-
2167-
bool should_free;
2168-
xmlChar *attr_value = php_libxml_attr_value(attr, &should_free);
2169-
2170-
ns->href = should_free ? attr_value : xmlStrdup(attr_value);
2171-
ns->prefix = attr->ns->prefix ? xmlStrdup(attr->name) : NULL;
2172-
ns->_private = attr;
2173-
if (attr->prev) {
2174-
attr->prev->next = attr->next;
2175-
} else {
2176-
node->properties = attr->next;
2177-
}
2178-
if (attr->next) {
2179-
attr->next->prev = attr->prev;
2180-
}
2181-
}
2182-
}
2183-
2184-
/* The default namespace is handled separately from the other namespaces in C14N.
2185-
* The default namespace is explicitly looked up while the other namespaces are
2186-
* deduplicated and compared to a list of visible namespaces. */
2187-
if (node->ns && !node->ns->prefix) {
2188-
/* Workaround for the behaviour where the xmlSearchNs() call inside c14n.c
2189-
* can return the current namespace. */
2190-
zend_hash_index_add_new_ptr(links, (zend_ulong) node | 1, node->ns);
2191-
node->ns = xmlSearchNs(node->doc, node, NULL);
2192-
} else if (node->ns) {
2193-
dom_add_synthetic_ns_decl(links, node, node->ns);
2194-
}
2195-
2196-
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
2197-
if (attr->ns && !php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
2198-
dom_add_synthetic_ns_decl_for_attr(links, node, attr->ns);
2199-
}
2200-
}
2201-
}
2202-
}
2203-
2204-
static void dom_relink_ns_decls(HashTable *links, xmlNodePtr root)
2205-
{
2206-
dom_relink_ns_decls_element(links, root);
2207-
2208-
xmlNodePtr base = root;
2209-
xmlNodePtr node = base->children;
2210-
while (node != NULL) {
2211-
dom_relink_ns_decls_element(links, node);
2212-
node = php_dom_next_in_tree_order(node, base);
2213-
}
2214-
}
2215-
2216-
static void dom_unlink_ns_decls(HashTable *links)
2217-
{
2218-
ZEND_HASH_MAP_FOREACH_NUM_KEY_VAL(links, zend_ulong h, zval *data) {
2219-
if (h & 1) {
2220-
xmlNodePtr node = (xmlNodePtr) (h ^ 1);
2221-
node->ns = Z_PTR_P(data);
2222-
} else {
2223-
xmlNodePtr node = (xmlNodePtr) h;
2224-
while (Z_LVAL_P(data)-- > 0) {
2225-
xmlNsPtr ns = node->nsDef;
2226-
node->nsDef = ns->next;
2227-
2228-
xmlAttrPtr attr = ns->_private;
2229-
if (attr) {
2230-
if (attr->prev) {
2231-
attr->prev->next = attr;
2232-
} else {
2233-
node->properties = attr;
2234-
}
2235-
if (attr->next) {
2236-
attr->next->prev = attr;
2237-
}
2238-
}
2239-
2240-
xmlFreeNs(ns);
2241-
}
2242-
}
2243-
} ZEND_HASH_FOREACH_END();
2244-
}
2245-
22462106
static int dom_canonicalize_node_parent_lookup_cb(void *user_data, xmlNodePtr node, xmlNodePtr parent)
22472107
{
22482108
xmlNodePtr root = user_data;

ext/dom/php_dom.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ void dom_set_document_ref_pointers_attr(xmlAttrPtr attr, php_libxml_ref_obj *doc
163163

164164
/* Prop getters by offset */
165165
zval *dom_get_prop_checked_offset(dom_object *obj, uint32_t offset, const char *name);
166+
/* Temporarily materialize namespace declarations as nsDef entries on the tree so
167+
* that libxml's native validators/canonicalizers can resolve prefixed QNames that
168+
* appear in element/attribute *content*. Modern DOM keeps declarations off the
169+
* tree (node->nsDef == NULL), which xmlSearchNs() cannot follow. Internal only. */
170+
void dom_relink_ns_decls(HashTable *links, xmlNodePtr root);
171+
void dom_unlink_ns_decls(HashTable *links);
166172
zval *dom_element_class_list_zval(dom_object *obj);
167173
zval *dom_parent_node_children(dom_object *obj);
168174

0 commit comments

Comments
 (0)