xref: /php-src/ext/dom/namespace_compat.c (revision 14b6c981)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Authors: Niels Dossche <nielsdos@php.net>                            |
14    +----------------------------------------------------------------------+
15 */
16 
17 #ifdef HAVE_CONFIG_H
18 #include "config.h"
19 #endif
20 
21 #include "php.h"
22 #if defined(HAVE_LIBXML) && defined(HAVE_DOM)
23 #include "php_dom.h"
24 #include "namespace_compat.h"
25 #include "internal_helpers.h"
26 
27 PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_html_magic_token = (const php_dom_ns_magic_token *) DOM_XHTML_NS_URI;
28 PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_mathml_magic_token = (const php_dom_ns_magic_token *) DOM_MATHML_NS_URI;
29 PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_svg_magic_token = (const php_dom_ns_magic_token *) DOM_SVG_NS_URI;
30 PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_xlink_magic_token = (const php_dom_ns_magic_token *) DOM_XLINK_NS_URI;
31 PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_xml_magic_token = (const php_dom_ns_magic_token *) DOM_XML_NS_URI;
32 PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_xmlns_magic_token = (const php_dom_ns_magic_token *) DOM_XMLNS_NS_URI;
33 
34 struct _php_dom_libxml_ns_mapper {
35 	php_libxml_private_data_header header;
36 	/* This is used almost all the time for HTML documents, so it makes sense to cache this. */
37 	xmlNsPtr html_ns;
38 	/* Used for every prefixless namespace declaration in XML, so also very common. */
39 	xmlNsPtr prefixless_xmlns_ns;
40 	HashTable uri_to_prefix_map;
41 };
42 
php_dom_libxml_ns_mapper_prefix_map_element_dtor(zval * zv)43 static void php_dom_libxml_ns_mapper_prefix_map_element_dtor(zval *zv)
44 {
45 	if (DOM_Z_IS_OWNED(zv)) {
46 		efree(Z_PTR_P(zv));
47 	}
48 }
49 
php_dom_libxml_ns_mapper_ensure_prefix_map(php_dom_libxml_ns_mapper * mapper,zend_string ** uri)50 static HashTable *php_dom_libxml_ns_mapper_ensure_prefix_map(php_dom_libxml_ns_mapper *mapper, zend_string **uri)
51 {
52 	zval *zv = zend_hash_find(&mapper->uri_to_prefix_map, *uri);
53 	HashTable *prefix_map;
54 	if (zv == NULL) {
55 		prefix_map = emalloc(sizeof(HashTable));
56 		zend_hash_init(prefix_map, 0, NULL, php_dom_libxml_ns_mapper_prefix_map_element_dtor, false);
57 		zval zv;
58 		ZVAL_ARR(&zv, prefix_map);
59 		zend_hash_add_new(&mapper->uri_to_prefix_map, *uri, &zv);
60 	} else {
61 		/* cast to Bucket* only works if this holds, I would prefer a static assert but we're stuck at C99. */
62 		ZEND_ASSERT(XtOffsetOf(Bucket, val) == 0);
63 		ZEND_ASSERT(Z_TYPE_P(zv) == IS_ARRAY);
64 		Bucket *bucket = (Bucket *) zv;
65 		/* Make sure we take the value from the key string that lives long enough. */
66 		*uri = bucket->key;
67 		prefix_map = Z_ARRVAL_P(zv);
68 	}
69 	return prefix_map;
70 }
71 
php_dom_libxml_ns_mapper_header_destroy(php_libxml_private_data_header * header)72 static void php_dom_libxml_ns_mapper_header_destroy(php_libxml_private_data_header *header)
73 {
74 	php_dom_libxml_ns_mapper_destroy((php_dom_libxml_ns_mapper *) header);
75 }
76 
php_dom_libxml_ns_mapper_create(void)77 PHP_DOM_EXPORT php_dom_libxml_ns_mapper *php_dom_libxml_ns_mapper_create(void)
78 {
79 	php_dom_libxml_ns_mapper *mapper = emalloc(sizeof(*mapper));
80 	mapper->header.dtor = php_dom_libxml_ns_mapper_header_destroy;
81 	mapper->html_ns = NULL;
82 	mapper->prefixless_xmlns_ns = NULL;
83 	zend_hash_init(&mapper->uri_to_prefix_map, 0, NULL, ZVAL_PTR_DTOR, false);
84 	return mapper;
85 }
86 
php_dom_libxml_ns_mapper_destroy(php_dom_libxml_ns_mapper * mapper)87 void php_dom_libxml_ns_mapper_destroy(php_dom_libxml_ns_mapper *mapper)
88 {
89 	zend_hash_destroy(&mapper->uri_to_prefix_map);
90 	efree(mapper);
91 }
92 
php_dom_libxml_ns_mapper_ensure_cached_ns(php_dom_libxml_ns_mapper * mapper,xmlNsPtr * ptr,const char * uri,size_t length,const php_dom_ns_magic_token * token)93 static xmlNsPtr php_dom_libxml_ns_mapper_ensure_cached_ns(php_dom_libxml_ns_mapper *mapper, xmlNsPtr *ptr, const char *uri, size_t length, const php_dom_ns_magic_token *token)
94 {
95 	if (EXPECTED(*ptr != NULL)) {
96 		return *ptr;
97 	}
98 
99 	zend_string *uri_str = zend_string_init(uri, length, false);
100 	*ptr = php_dom_libxml_ns_mapper_get_ns(mapper, NULL, uri_str);
101 	(*ptr)->_private = (void *) token;
102 	zend_string_release_ex(uri_str, false);
103 	return *ptr;
104 }
105 
php_dom_libxml_ns_mapper_ensure_html_ns(php_dom_libxml_ns_mapper * mapper)106 PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_ensure_html_ns(php_dom_libxml_ns_mapper *mapper)
107 {
108 	return php_dom_libxml_ns_mapper_ensure_cached_ns(mapper, &mapper->html_ns, DOM_XHTML_NS_URI, sizeof(DOM_XHTML_NS_URI) - 1, php_dom_ns_is_html_magic_token);
109 }
110 
php_dom_libxml_ns_mapper_ensure_prefixless_xmlns_ns(php_dom_libxml_ns_mapper * mapper)111 PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_ensure_prefixless_xmlns_ns(php_dom_libxml_ns_mapper *mapper)
112 {
113 	return php_dom_libxml_ns_mapper_ensure_cached_ns(mapper, &mapper->prefixless_xmlns_ns, DOM_XMLNS_NS_URI, sizeof(DOM_XMLNS_NS_URI) - 1, php_dom_ns_is_xmlns_magic_token);
114 }
115 
dom_create_owned_ns(zend_string * prefix,zend_string * uri)116 static xmlNsPtr dom_create_owned_ns(zend_string *prefix, zend_string *uri)
117 {
118 	ZEND_ASSERT(prefix != NULL);
119 	ZEND_ASSERT(uri != NULL);
120 
121 	xmlNsPtr ns = emalloc(sizeof(*ns));
122 	memset(ns, 0, sizeof(*ns));
123 	ns->type = XML_LOCAL_NAMESPACE;
124 	/* These two strings are kept alive because they're the hash table keys that lead to this entry. */
125 	ns->prefix = ZSTR_LEN(prefix) == 0 ? NULL : BAD_CAST ZSTR_VAL(prefix);
126 	ns->href = BAD_CAST ZSTR_VAL(uri);
127 	/* Note ns->context is unused in libxml2 at the moment, and if it were used it would be for
128 	 * LIBXML_NAMESPACE_DICT which is opt-in anyway. */
129 
130 	return ns;
131 }
132 
php_dom_libxml_ns_mapper_get_ns(php_dom_libxml_ns_mapper * mapper,zend_string * prefix,zend_string * uri)133 PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_get_ns(php_dom_libxml_ns_mapper *mapper, zend_string *prefix, zend_string *uri)
134 {
135 	if (uri == NULL) {
136 		uri = zend_empty_string;
137 	}
138 
139 	if (prefix == NULL) {
140 		prefix = zend_empty_string;
141 	}
142 
143 	if (ZSTR_LEN(prefix) == 0 && ZSTR_LEN(uri) == 0) {
144 		return NULL;
145 	}
146 
147 	HashTable *prefix_map = php_dom_libxml_ns_mapper_ensure_prefix_map(mapper, &uri);
148 	xmlNsPtr found = zend_hash_find_ptr(prefix_map, prefix);
149 	if (found != NULL) {
150 		return found;
151 	}
152 
153 	xmlNsPtr ns = dom_create_owned_ns(prefix, uri);
154 
155 	zval new_zv;
156 	DOM_Z_OWNED(&new_zv, ns);
157 	zend_hash_add_new(prefix_map, prefix, &new_zv);
158 
159 	return ns;
160 }
161 
php_dom_libxml_ns_mapper_get_ns_raw_prefix_string(php_dom_libxml_ns_mapper * mapper,const xmlChar * prefix,size_t prefix_len,zend_string * uri)162 PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_get_ns_raw_prefix_string(php_dom_libxml_ns_mapper *mapper, const xmlChar *prefix, size_t prefix_len, zend_string *uri)
163 {
164 	xmlNsPtr ns;
165 	if (prefix_len == 0) {
166 		/* Fast path */
167 		ns = php_dom_libxml_ns_mapper_get_ns(mapper, zend_empty_string, uri);
168 	} else {
169 		zend_string *prefix_str = zend_string_init((const char *) prefix, prefix_len, false);
170 		ns = php_dom_libxml_ns_mapper_get_ns(mapper, prefix_str, uri);
171 		zend_string_release_ex(prefix_str, false);
172 	}
173 	return ns;
174 }
175 
php_dom_libxml_ns_mapper_get_ns_raw_strings_ex(php_dom_libxml_ns_mapper * mapper,const char * prefix,size_t prefix_len,const char * uri,size_t uri_len)176 static xmlNsPtr php_dom_libxml_ns_mapper_get_ns_raw_strings_ex(php_dom_libxml_ns_mapper *mapper, const char *prefix, size_t prefix_len, const char *uri, size_t uri_len)
177 {
178 	zend_string *prefix_str = zend_string_init(prefix, prefix_len, false);
179 	zend_string *uri_str = zend_string_init(uri, uri_len, false);
180 	xmlNsPtr ns = php_dom_libxml_ns_mapper_get_ns(mapper, prefix_str, uri_str);
181 	zend_string_release_ex(prefix_str, false);
182 	zend_string_release_ex(uri_str, false);
183 	return ns;
184 }
185 
php_dom_libxml_ns_mapper_get_ns_raw_strings(php_dom_libxml_ns_mapper * mapper,const char * prefix,const char * uri)186 static zend_always_inline xmlNsPtr php_dom_libxml_ns_mapper_get_ns_raw_strings(php_dom_libxml_ns_mapper *mapper, const char *prefix, const char *uri)
187 {
188 	return php_dom_libxml_ns_mapper_get_ns_raw_strings_ex(mapper, prefix, strlen(prefix), uri, strlen(uri));
189 }
190 
php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(php_dom_libxml_ns_mapper * mapper,const char * prefix,const char * uri)191 PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(php_dom_libxml_ns_mapper *mapper, const char *prefix, const char *uri)
192 {
193 	if (prefix == NULL) {
194 		prefix = "";
195 	}
196 	if (uri == NULL) {
197 		uri = "";
198 	}
199 	return php_dom_libxml_ns_mapper_get_ns_raw_strings(mapper, prefix, uri);
200 }
201 
php_dom_libxml_ns_mapper_store_and_normalize_parsed_ns(php_dom_libxml_ns_mapper * mapper,xmlNsPtr ns)202 static xmlNsPtr php_dom_libxml_ns_mapper_store_and_normalize_parsed_ns(php_dom_libxml_ns_mapper *mapper, xmlNsPtr ns)
203 {
204 	ZEND_ASSERT(ns != NULL);
205 
206 	zend_string *href_str = zend_string_init((const char *) ns->href, xmlStrlen(ns->href), false);
207 	zend_string *href_str_orig = href_str;
208 	HashTable *prefix_map = php_dom_libxml_ns_mapper_ensure_prefix_map(mapper, &href_str);
209 	zend_string_release_ex(href_str_orig, false);
210 
211 	const char *prefix = (const char *) ns->prefix;
212 	size_t prefix_len;
213 	if (prefix == NULL) {
214 		prefix = "";
215 		prefix_len = 0;
216 	} else {
217 		prefix_len = xmlStrlen(ns->prefix);
218 	}
219 
220 	zval *zv = zend_hash_str_find_ptr(prefix_map, prefix, prefix_len);
221 	if (zv != NULL) {
222 		return Z_PTR_P(zv);
223 	}
224 
225 	zval new_zv;
226 	DOM_Z_UNOWNED(&new_zv, ns);
227 	zend_hash_str_add_new(prefix_map, prefix, prefix_len, &new_zv);
228 
229 	return ns;
230 }
231 
php_dom_libxml_ns_mapper_header(php_dom_libxml_ns_mapper * mapper)232 PHP_DOM_EXPORT php_libxml_private_data_header *php_dom_libxml_ns_mapper_header(php_dom_libxml_ns_mapper *mapper)
233 {
234 	return mapper == NULL ? NULL : &mapper->header;
235 }
236 
237 typedef struct {
238 	/* Fast lookup for created mappings. */
239 	HashTable old_ns_to_new_ns_ptr;
240 	/* It is common that the last created mapping will be used for a while,
241 	 * cache it too to bypass the hash table. */
242 	xmlNsPtr last_mapped_src, last_mapped_dst;
243 	php_dom_libxml_ns_mapper *ns_mapper;
244 } dom_libxml_reconcile_ctx;
245 
php_dom_ns_compat_mark_attribute(php_dom_libxml_ns_mapper * mapper,xmlNodePtr node,xmlNsPtr ns)246 PHP_DOM_EXPORT xmlAttrPtr php_dom_ns_compat_mark_attribute(php_dom_libxml_ns_mapper *mapper, xmlNodePtr node, xmlNsPtr ns)
247 {
248 	xmlNsPtr xmlns_ns;
249 	const xmlChar *name;
250 	if (ns->prefix != NULL) {
251 		xmlns_ns = php_dom_libxml_ns_mapper_get_ns_raw_strings(mapper, "xmlns", DOM_XMLNS_NS_URI);
252 		name = ns->prefix;
253 	} else {
254 		xmlns_ns = php_dom_libxml_ns_mapper_ensure_prefixless_xmlns_ns(mapper);
255 		name = BAD_CAST "xmlns";
256 	}
257 
258 	ZEND_ASSERT(xmlns_ns != NULL);
259 
260 	return xmlSetNsProp(node, xmlns_ns, name, ns->href);
261 }
262 
php_dom_ns_compat_mark_attribute_list(php_dom_libxml_ns_mapper * mapper,xmlNodePtr node)263 PHP_DOM_EXPORT void php_dom_ns_compat_mark_attribute_list(php_dom_libxml_ns_mapper *mapper, xmlNodePtr node)
264 {
265 	if (node->nsDef == NULL) {
266 		return;
267 	}
268 
269 	/* We want to prepend at the front, but in order of the namespace definitions.
270 	 * So temporarily unlink the existing properties and add them again at the end. */
271 	xmlAttrPtr attr = node->properties;
272 	node->properties = NULL;
273 
274 	xmlNsPtr ns = node->nsDef;
275 	xmlAttrPtr last_added = NULL;
276 	do {
277 		last_added = php_dom_ns_compat_mark_attribute(mapper, node, ns);
278 		php_dom_libxml_ns_mapper_store_and_normalize_parsed_ns(mapper, ns);
279 		xmlNsPtr next = ns->next;
280 		ns->next = NULL;
281 		php_libxml_set_old_ns(node->doc, ns);
282 		ns = next;
283 	} while (ns != NULL);
284 
285 	if (last_added != NULL) {
286 		/* node->properties now points to the first namespace declaration attribute. */
287 		if (attr != NULL) {
288 			last_added->next = attr;
289 			attr->prev = last_added;
290 		}
291 	} else {
292 		/* Nothing added, so nothing changed. Only really possible on OOM. */
293 		node->properties = attr;
294 	}
295 
296 	node->nsDef = NULL;
297 }
298 
php_dom_ns_is_fast_ex(xmlNsPtr ns,const php_dom_ns_magic_token * magic_token)299 PHP_DOM_EXPORT bool php_dom_ns_is_fast_ex(xmlNsPtr ns, const php_dom_ns_magic_token *magic_token)
300 {
301 	ZEND_ASSERT(ns != NULL);
302 	/* cached for fast checking */
303 	if (ns->_private == magic_token) {
304 		return true;
305 	} else if (ns->_private != NULL) {
306 		/* Other token stored */
307 		return false;
308 	}
309 	/* Slow path */
310 	if (xmlStrEqual(ns->href, BAD_CAST magic_token)) {
311 		ns->_private = (void *) magic_token;
312 		return true;
313 	}
314 	return false;
315 }
316 
php_dom_ns_is_fast(const xmlNode * nodep,const php_dom_ns_magic_token * magic_token)317 PHP_DOM_EXPORT bool php_dom_ns_is_fast(const xmlNode *nodep, const php_dom_ns_magic_token *magic_token)
318 {
319 	ZEND_ASSERT(nodep != NULL);
320 	xmlNsPtr ns = nodep->ns;
321 	if (ns != NULL) {
322 		return php_dom_ns_is_fast_ex(ns, magic_token);
323 	}
324 	return false;
325 }
326 
php_dom_ns_is_html_and_document_is_html(const xmlNode * nodep)327 PHP_DOM_EXPORT bool php_dom_ns_is_html_and_document_is_html(const xmlNode *nodep)
328 {
329 	ZEND_ASSERT(nodep != NULL);
330 	return nodep->doc && nodep->doc->type == XML_HTML_DOCUMENT_NODE && php_dom_ns_is_fast(nodep, php_dom_ns_is_html_magic_token);
331 }
332 
333 /* will rename prefixes if there is a declaration with the same prefix but different uri. */
php_dom_reconcile_attribute_namespace_after_insertion(xmlAttrPtr attrp)334 PHP_DOM_EXPORT void php_dom_reconcile_attribute_namespace_after_insertion(xmlAttrPtr attrp)
335 {
336 	ZEND_ASSERT(attrp != NULL);
337 
338 	if (attrp->ns != NULL) {
339 		/* Try to link to an existing namespace. If that won't work, reconcile. */
340 		xmlNodePtr nodep = attrp->parent;
341 		xmlNsPtr matching_ns = xmlSearchNs(nodep->doc, nodep, attrp->ns->prefix);
342 		if (matching_ns && xmlStrEqual(matching_ns->href, attrp->ns->href)) {
343 			/* Doesn't leak because this doesn't define the declaration. */
344 			attrp->ns = matching_ns;
345 		} else {
346 			if (attrp->ns->prefix != NULL) {
347 				/* Note: explicitly use the legacy reconciliation as it mostly (i.e. as good as it gets for legacy DOM)
348 				* does the right thing for attributes. */
349 				xmlReconciliateNs(nodep->doc, nodep);
350 			}
351 		}
352 	}
353 }
354 
dom_mangle_pointer_for_key(void * ptr)355 static zend_always_inline zend_long dom_mangle_pointer_for_key(void *ptr)
356 {
357 	zend_ulong value = (zend_ulong) (uintptr_t) ptr;
358 	/* Shift 3/4 for better hash distribution because the low 3/4 bits are always 0. */
359 #if SIZEOF_ZEND_LONG == 8
360 	return value >> 4;
361 #else
362 	return value >> 3;
363 #endif
364 }
365 
php_dom_libxml_reconcile_modern_single_node(dom_libxml_reconcile_ctx * ctx,xmlNodePtr ns_holder,xmlNodePtr node)366 static zend_always_inline void php_dom_libxml_reconcile_modern_single_node(dom_libxml_reconcile_ctx *ctx, xmlNodePtr ns_holder, xmlNodePtr node)
367 {
368 	ZEND_ASSERT(node->ns != NULL);
369 
370 	if (node->ns == ctx->last_mapped_src) {
371 		node->ns = ctx->last_mapped_dst;
372 		return;
373 	}
374 
375 	/* If the namespace is the same as in the map, we're good. */
376 	xmlNsPtr new_ns = zend_hash_index_find_ptr(&ctx->old_ns_to_new_ns_ptr, dom_mangle_pointer_for_key(node->ns));
377 	if (new_ns == NULL) {
378 		/* We have to create an alternative declaration, and we'll add it to the map. */
379 		const char *prefix = (const char *) node->ns->prefix;
380 		const char *href = (const char *) node->ns->href;
381 		new_ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ctx->ns_mapper, prefix, href);
382 		zend_hash_index_add_new_ptr(&ctx->old_ns_to_new_ns_ptr, dom_mangle_pointer_for_key(node->ns), new_ns);
383 		ctx->last_mapped_src = node->ns;
384 		ctx->last_mapped_dst = new_ns;
385 		node->ns = new_ns;
386 	} else if (node->ns != new_ns) {
387 		/* The namespace is different, so we have to replace it. */
388 		node->ns = new_ns;
389 	}
390 }
391 
dom_libxml_reconcile_fast_element_skip(xmlNodePtr node)392 static zend_always_inline bool dom_libxml_reconcile_fast_element_skip(xmlNodePtr node)
393 {
394 	/* Fast path: this is a lone element and the namespace is defined by the node (or the namespace is NULL). */
395 	ZEND_ASSERT(node->type == XML_ELEMENT_NODE);
396 	return node->children == NULL && node->properties == NULL && node->ns == node->nsDef;
397 }
398 
php_dom_libxml_reconcile_modern_single_element_node(dom_libxml_reconcile_ctx * ctx,xmlNodePtr node)399 static zend_always_inline void php_dom_libxml_reconcile_modern_single_element_node(dom_libxml_reconcile_ctx *ctx, xmlNodePtr node)
400 {
401 	ZEND_ASSERT(node->type == XML_ELEMENT_NODE);
402 
403 	/* Since this is modern DOM, the declarations are not on the node and thus there's nothing to add from nsDef. */
404 	ZEND_ASSERT(node->nsDef == NULL);
405 
406 	if (node->ns != NULL) {
407 		php_dom_libxml_reconcile_modern_single_node(ctx, node, node);
408 	}
409 
410 	for (xmlAttrPtr attr = node->properties; attr != NULL; attr = attr->next) {
411 		if (attr->ns != NULL) {
412 			php_dom_libxml_reconcile_modern_single_node(ctx, node, (xmlNodePtr) attr);
413 		}
414 	}
415 }
416 
php_dom_libxml_reconcile_modern(php_dom_libxml_ns_mapper * ns_mapper,xmlNodePtr node)417 PHP_DOM_EXPORT void php_dom_libxml_reconcile_modern(php_dom_libxml_ns_mapper *ns_mapper, xmlNodePtr node)
418 {
419 	if (node->type == XML_ATTRIBUTE_NODE) {
420 		if (node->ns != NULL) {
421 			node->ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, (const char *) node->ns->prefix, (const char *) node->ns->href);
422 		}
423 		return;
424 	}
425 
426 	if (node->type != XML_ELEMENT_NODE || dom_libxml_reconcile_fast_element_skip(node)) {
427 		return;
428 	}
429 
430 	dom_libxml_reconcile_ctx ctx;
431 	zend_hash_init(&ctx.old_ns_to_new_ns_ptr, 0, NULL, NULL, 0);
432 	ctx.last_mapped_src = NULL;
433 	ctx.last_mapped_dst = NULL;
434 	ctx.ns_mapper = ns_mapper;
435 
436 	php_dom_libxml_reconcile_modern_single_element_node(&ctx, node);
437 
438 	xmlNodePtr base = node;
439 	node = node->children;
440 	while (node != NULL) {
441 		ZEND_ASSERT(node != base);
442 
443 		if (node->type == XML_ELEMENT_NODE) {
444 			php_dom_libxml_reconcile_modern_single_element_node(&ctx, node);
445 
446 			if (node->children) {
447 				node = node->children;
448 				continue;
449 			}
450 		}
451 
452 		node = php_dom_next_in_tree_order(node, base);
453 	}
454 
455 	zend_hash_destroy(&ctx.old_ns_to_new_ns_ptr);
456 }
457 
php_dom_get_in_scope_ns(php_dom_libxml_ns_mapper * ns_mapper,const xmlNode * node)458 PHP_DOM_EXPORT php_dom_in_scope_ns php_dom_get_in_scope_ns(php_dom_libxml_ns_mapper *ns_mapper, const xmlNode *node)
459 {
460 	ZEND_ASSERT(node != NULL);
461 
462 	php_dom_in_scope_ns in_scope_ns;
463 	in_scope_ns.origin_is_ns_compat = true;
464 
465 	/* libxml fetches all nsDef items from bottom to top - left to right, ignoring prefixes already in the list.
466 	 * We don't have nsDef, but we can use the ns pointer (as that is necessarily in scope),
467 	 * and check the xmlns attributes. */
468 	HashTable tmp_prefix_to_ns_table;
469 	zend_hash_init(&tmp_prefix_to_ns_table, 0, NULL, NULL, false);
470 	zend_hash_real_init_mixed(&tmp_prefix_to_ns_table);
471 
472 	for (const xmlNode *cur = node; cur != NULL; cur = cur->parent) {
473 		if (cur->type == XML_ELEMENT_NODE) {
474 			/* Register namespace of element */
475 			if (cur->ns != NULL && cur->ns->prefix != NULL) {
476 				const char *prefix = (const char *) cur->ns->prefix;
477 				zend_hash_str_add_ptr(&tmp_prefix_to_ns_table, prefix, strlen(prefix), cur->ns);
478 			}
479 
480 			/* Register xmlns attributes */
481 			for (const xmlAttr *attr = cur->properties; attr != NULL; attr = attr->next) {
482 				if (attr->ns != NULL && attr->ns->prefix != NULL && php_dom_ns_is_fast_ex(attr->ns, php_dom_ns_is_xmlns_magic_token)
483 					&& attr->children != NULL && attr->children->content != NULL) {
484 					/* This attribute declares a namespace, get the relevant instance.
485 					 * The declared namespace is not the same as the namespace of this attribute (which is xmlns). */
486 					const char *prefix = (const char *) attr->name;
487 					xmlNsPtr ns = php_dom_libxml_ns_mapper_get_ns_raw_strings(ns_mapper, prefix, (const char *) attr->children->content);
488 					zend_hash_str_add_ptr(&tmp_prefix_to_ns_table, prefix, strlen(prefix), ns);
489 				}
490 			}
491 		}
492 	}
493 
494 	in_scope_ns.count = zend_hash_num_elements(&tmp_prefix_to_ns_table);
495 	in_scope_ns.list = safe_emalloc(in_scope_ns.count, sizeof(xmlNsPtr), 0);
496 
497 	size_t index = 0;
498 	xmlNsPtr ns;
499 	ZEND_HASH_MAP_FOREACH_PTR(&tmp_prefix_to_ns_table, ns) {
500 		in_scope_ns.list[index++] = ns;
501 	} ZEND_HASH_FOREACH_END();
502 
503 	zend_hash_destroy(&tmp_prefix_to_ns_table);
504 
505 	return in_scope_ns;
506 }
507 
php_dom_get_in_scope_ns_legacy(const xmlNode * node)508 PHP_DOM_EXPORT php_dom_in_scope_ns php_dom_get_in_scope_ns_legacy(const xmlNode *node)
509 {
510 	ZEND_ASSERT(node != NULL);
511 
512 	php_dom_in_scope_ns in_scope_ns;
513 	in_scope_ns.origin_is_ns_compat = false;
514 	in_scope_ns.list = xmlGetNsList(node->doc, node);
515 	in_scope_ns.count = 0;
516 
517 	if (in_scope_ns.list != NULL) {
518 		while (in_scope_ns.list[in_scope_ns.count] != NULL) {
519 			in_scope_ns.count++;
520 		}
521 	}
522 
523 	return in_scope_ns;
524 }
525 
php_dom_in_scope_ns_destroy(php_dom_in_scope_ns * in_scope_ns)526 PHP_DOM_EXPORT void php_dom_in_scope_ns_destroy(php_dom_in_scope_ns *in_scope_ns)
527 {
528 	ZEND_ASSERT(in_scope_ns != NULL);
529 	if (in_scope_ns->origin_is_ns_compat) {
530 		efree(in_scope_ns->list);
531 	} else {
532 		xmlFree(in_scope_ns->list);
533 	}
534 }
535 
536 #endif  /* HAVE_LIBXML && HAVE_DOM */
537